Skip to content

Sync/upstream main 20260604#5

Merged
msajidmansoori12 merged 18 commits into
mainfrom
sync/upstream-main-20260604
Jun 4, 2026
Merged

Sync/upstream main 20260604#5
msajidmansoori12 merged 18 commits into
mainfrom
sync/upstream-main-20260604

Conversation

@msajidmansoori12

@msajidmansoori12 msajidmansoori12 commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

Release Notes

  • New Features

    • Transform and apply commands now accept stages as positional arguments instead of --stage flag
    • Instructions file support with --instructions-file and --force for stage reconciliation
    • Cluster-scoped resource filtering via --skip-cluster-scoped flag
    • Embedded kustomize runner for improved performance
    • Version command now displays kustomize version
    • Plugin shell completion support
  • Improvements

    • Enhanced error messages for invalid/unresolved stages
    • Case-insensitive output format handling in validation
    • Build automation via GitHub Actions pipeline
  • Tests

    • Comprehensive test coverage for new staging workflows and instructions-file functionality

msajidmansoori12 and others added 18 commits May 20, 2026 13:39
…ion. (migtools#378)

* First draft for building crane binaries

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Temporary testing on fork

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Update workflow to not include checksum files as they are present in zip files already

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Add readme download section

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Rename file and update readme

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Finalize changes

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

---------

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>
…s#383)

* fix: Add tests for cluster-scoped resources in transform

* fix comments

* fixed comments
* embed kustomize to crane

* fix resource order

* fix coderabbit comments

* Removed --load-restrictor and --enable-alpha-plugins flags and some other kubectl kustomize references
* Add --skip-cluster-scoped flag to apply

* fix comments

* fix coderabbit comments

* fix coderabbit comment
* Add --config-file CLI flag to accept transform stage dir

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Add functions to load and validate config files

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Add precheck to make sure --config-file and --stage are not used at once

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Wire new stages creation from config file

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Add basic unit tests

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Add more edge cases unit tests

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Add a method to reconcile existing transform stages

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Update function comments

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Add more tests and make the schema check for transform file stricter

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Update config tests to table driven tests

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Add unit test to cover scenario of --stage and --config-file both being provided

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* include wrapped error for failing to unmarshal config file

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Hardening to not accept multi doc yaml

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* nit addressed

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Add cleanup for workdir with --force option

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Rename functions to match naming for instruction file

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Update rest of the code to use --instructions-file

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

---------

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>
* Add validate command to framework

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Add validatedir to scenario paths

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Automate validate alternative GV suggestion test

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Re

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

---------

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>
* Added kustomize version

* fix coderabbit comments

* Fix kustomize CLI version
…tion (migtools#407)

* Update e2e test for CronJob PVC migration: Comment out PVC data integrity checks due to flaky behavior on Linux. Investigation planned for root cause analysis post-release.

Signed-off-by: midays <midays@redhat.com>

* Update e2e test for CronJob PVC migration: Add reference to BUG migtools#408 in test description and improve comments regarding PVC data integrity checks. Investigation into flaky behavior on Linux continues.

Signed-off-by: midays <midays@redhat.com>

---------

Signed-off-by: midays <midays@redhat.com>
…ile (migtools#433)

* Add function to run transform with instructions file

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Add instructions-file basic tier0 test

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

---------

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>
* Transform stages for all plugins by default

Default execution of `crane transform` creates stages for all available
plugins (instead of just default KubernetePlugin).

This could be optionally restricted with --skip-plugins arg.

Related to migtools#370 (Task 1)

Signed-off-by: Marek Aufart <maufart@redhat.com>

* Update Plugin name path sanitization

Signed-off-by: Marek Aufart <maufart@redhat.com>

* Update plugin&stage names validation

Signed-off-by: Marek Aufart <maufart@redhat.com>

* Update relpath stage safe check

Signed-off-by: Marek Aufart <maufart@redhat.com>

* Implement positional arguments for multi-stage selection (Issue migtools#410)

Changes:
- Replace --stage flag with positional arguments for transform and apply commands
- Add support for multiple stages: crane transform stage1 stage2
- Support plugin name resolution: crane transform KubernetesPlugin
- Implement duplicate plugin detection with helpful error messages
- Update StageSelector to use []string instead of single string
- Add resolveAndValidateStages() with plugin name → stage dir resolution

Usage:
  crane transform [stage...]
  crane apply [stage...]

Examples:
  crane transform                              # all stages
  crane transform 10_KubernetesPlugin          # one stage by dir name
  crane transform KubernetesPlugin             # one stage by plugin name
  crane transform stage1 stage2                # multiple stages
  crane apply 10_KubernetesPlugin OpenshiftPlugin

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* Update tests for positional arguments

- Update StageSelector usage from Stage string to Stages []string
- Add tests for multiple stage selection
- Add tests for plugin name matching
- Update apply tests to use RequestedStages instead of Flags.Stage
- All tests passing

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* Fix stage plugin order counting

Signed-off-by: Marek Aufart <maufart@redhat.com>

* Fix E2E tests with stage as positional arg

Signed-off-by: Marek Aufart <maufart@redhat.com>

* Fix custom stage creation without plugin

Signed-off-by: Marek Aufart <maufart@redhat.com>

* Fix previous stage output

Signed-off-by: Marek Aufart <maufart@redhat.com>

* Refactoring & deduplication

Signed-off-by: Marek Aufart <maufart@redhat.com>

* Add automatic custom stage order lookup

Signed-off-by: Marek Aufart <maufart@redhat.com>

* Refactoring and cleanup

Signed-off-by: Marek Aufart <maufart@redhat.com>

* Add requested stages validation to apply

Signed-off-by: Marek Aufart <maufart@redhat.com>

* Update go mod

Signed-off-by: Marek Aufart <maufart@redhat.com>

* Unify find Stages dir function

Signed-off-by: Marek Aufart <maufart@redhat.com>

---------

Signed-off-by: Marek Aufart <maufart@redhat.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* Add polarion case id for tier0 test

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Add test id for validate alternative GV testcase

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Add tier1 case for force reconcile migration

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Add testcase id from polarion

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Add check for stale .work/99_OrphanStage deletion post force reconcile

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

---------

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>
The previous path construction using filepath.Join(config.CraneBin, "..", "..", "scripts", "capture-api-surface.sh") was incorrect because:
- config.CraneBin is the path to the crane binary itself, not a directory
- Going up two levels from the binary didn't reach the repository root

Fixed by:
- Using filepath.Dir() to get the directory of the crane binary
- Going up one more level to reach the repository root
- Then joining with "scripts" and the script name

This ensures the script is found at the correct path during test execution.
"
* Add autocompletion for transform plugins

Signed-off-by: Marek Aufart <maufart@redhat.com>

* Raise error on skipPlugins

Signed-off-by: Marek Aufart <maufart@redhat.com>

* Add full transform completion test

Signed-off-by: Marek Aufart <maufart@redhat.com>

---------

Signed-off-by: Marek Aufart <maufart@redhat.com>
* Add test for crane validate alternate GV in offline mode

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

* Add doc comment and simple unit test

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>

---------

Signed-off-by: M Sajid Mansoori <mmansoor@redhat.com>
@msajidmansoori12 msajidmansoori12 self-assigned this Jun 4, 2026
@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR refactors crane's transform and apply commands from single-stage --stage flag selection to positional multi-stage arguments, adds YAML instructions-file support for stage orchestration, replaces external kubectl/oc kustomize execution with an embedded kustomize runner, and implements optional cluster-scoped resource filtering during apply. The changes include comprehensive test coverage for new stage resolution logic, E2E tests validating instructions-file migration and validate command enhancements, and infrastructure updates for binary releases.

Changes

Core multi-stage and instructions orchestration

Layer / File(s) Summary
Instructions file data model and loading
internal/transform/instructions.go, internal/transform/instructions_test.go
New InstructionsFile struct with Stages field; LoadInstructions parses YAML with strict decoding and user-friendly error messages; ValidateInstructions enforces token character validation and deduplication; GenerateStageDirNames generates deterministic numeric-prefixed directory names.
Multi-stage selector API and filtering
internal/transform/stages.go, internal/transform/stages_test.go
StageSelector changed from single Stage string to Stages []string; FilterStages now matches by directory name or plugin name, deduplicates by directory, and returns filtered subset or all stages when empty.
Transform command positional args and instructions-file integration
cmd/transform/transform.go, cmd/transform/transform_test.go
Options.RequestedStages captures positional arguments; new Flags fields Force and InstructionsFile; three-mode orchestration (instructions-file with reconciliation, positional multi-stage, or auto-discovery/defaults); reconcileInstructionStages enforces desired vs actual stage directories with --force deletion of extras.
Apply command positional args and cluster-scoped filtering
cmd/apply/apply.go, cmd/apply/apply_test.go
Options.RequestedStages for positional stage args; Flags removes Stage, adds SkipClusterScoped; run() resolves stages by directory/plugin name and passes filtering flag to applier.

Embedded kustomize runner and orchestration

Layer / File(s) Summary
Embedded kustomize runner implementation
internal/kustomize/runner.go, internal/kustomize/runner_test.go
New Runner type with Build(dir) using in-process krusty API instead of shell commands; buildOptions() parses --enable-helm, --helm-command, and --env arguments; setEnvVars() manages temporary environment variables with deferred restoration.
Kustomize argument validation and allowlist
internal/kustomize/args.go, internal/kustomize/args_test.go
AllowedKustomizeArgs restricted to Helm-related flags (--enable-helm, --env, -e, --helm-command); removed support for --load-restrictor and --enable-alpha-plugins; validation enforces whitelist and token character rules.
Cluster-scoped resource filtering in apply
internal/apply/kustomize.go, internal/apply/kustomize_test.go
New filterClusterScopedResources decodes multi-document YAML and skips documents with empty namespace; integrated into single-stage and multi-stage apply when SkipClusterScoped is true; includes comprehensive test coverage for filtering scenarios.
Orchestrator refactored to use embedded kustomize
internal/transform/orchestrator.go, internal/transform/orchestrator_test.go
applyStageTransforms now uses kustomize.Runner.Build(stageDir) instead of external commands; imports updated; added integration test for cluster-scoped resource persistence across multi-stage pipelines.

Default stage generation from plugins

Layer / File(s) Summary
Plugin discovery and name extraction helpers
cmd/transform/listplugins/listplugins.go, cmd/transform/listplugins/listplugins_test.go, cmd/transform/completion_test.go
New GetPluginNames exported function filters plugins and returns metadata names; getFilteredPlugins ensures absolute path resolution; GetPlugins no longer pre-seeds built-in Kubernetes plugin; updated plugin-discovery completion tests.
Stage name validation and directory creation
cmd/transform/transform.go, cmd/transform/transform_test.go
New validateStageNameToken checks for path-traversal and unsafe characters; createStageDirectory creates stages with path-traversal protection and tracks in orchestrator.NewlyCreatedStages; deterministic plugin ordering via sort; when no stages exist, auto-creates from all plugins.
Stage resolution for positional and base-name inputs
cmd/transform/transform.go, cmd/transform/transform_test.go
resolveAndValidateStages maps user requests (exact dir names, plugin names, custom base names) to resolved stages with deduplication, priority assignment (10-step increments), lazy plugin loading, and clear error messages for ambiguity or missing dependencies.

Test framework, utilities, and CLI enhancements

Layer / File(s) Summary
E2E test framework enhancements
e2e-tests/framework/crane.go, e2e-tests/framework/scenario.go
New ValidateOptions struct and CraneRunner.Validate method; TransformStage and new TransformWithInstructionsFile pass stages as positional arguments; ScenarioPaths adds ValidateDir.
Version and validate command enhancements
cmd/version/version.go, cmd/version/version_test.go, cmd/validate/validate.go, cmd/validate/validate_test.go
Version command now outputs kustomize version from buildinfo.KustomizeVersion; validate command lowercases output format and errors on empty manifest input with guard before manifest processing.
Kustomize version detection via build info
internal/buildinfo/buildinfo.go, internal/buildinfo/buildinfo_test.go
New KustomizeVersion variable initialized from readKustomizeVersion() which inspects runtime build info for sigs.k8s.io/kustomize/kustomize/v5 module version or returns "unknown".
Test helper utilities for E2E tests
e2e-tests/utils/utils.go, e2e-tests/utils/utils_test.go
New TestdataFilePath and CaptureAPISurfaceScriptPath helpers resolve repository-relative paths via runtime.Caller independent of working directory; used by new E2E tests.
Remove deprecated command helpers
internal/file/file_helper.go, internal/transform/test_helpers.go
Removed GetKustomizeCommand() and ValidateKubectlAvailable() as part of embedded kustomize transition; updated test helpers to unconditionally report kustomize available.

End-to-end tests for new features

Layer / File(s) Summary
Instructions-file mode E2E tests
e2e-tests/testdata/basic-instructions-file.yaml, e2e-tests/tests/mta_828_instructions_file_migration_test.go, e2e-tests/tests/mta_830_instructions_file_force_reconcile_test.go
MTA-828: validates --instructions-file migration with test data defining KubernetesPlugin and CustomStage. MTA-830: validates --force reconciliation removes orphan stages and overwrites preexisting files.
Validate command with API version suggestions E2E tests
e2e-tests/tests/mta_829_validate_alternative_gv_suggestion_test.go, e2e-tests/tests/mta_832_validate_alternative_gv_suggestion_offline_test.go
MTA-829 (live mode) and MTA-832 (offline mode) validate that validate command detects deprecated apiVersion and suggests replacement in report.json and failure YAML.
PVC data integrity verification temporarily skipped
e2e-tests/tests/mta_813_cronJob_PVC_test.go
Commented out PVC data integrity check pending resolution of flaky Linux behavior (crane issue #408); test labels updated to reference BUG #408.
Test log message update for positional stage argument
e2e-tests/tests/mta_827_custom_transformation_stage_test.go
Updated log message to reflect running crane transform with specific stage as positional argument rather than generic --stage phrasing.

Build/release and dependency management

Layer / File(s) Summary
GitHub Actions build-release-binaries workflow
.github/workflows/build-release-binaries.yml
New workflow for Go 1.25 cross-compilation targeting linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64; per-artifact uploads; tag-gated release draft creation with combined SHA-256 checksums.
Go module dependency updates
go.mod
Added direct requires for sigs.k8s.io/kustomize/api v0.21.1 and sigs.k8s.io/kustomize/kustomize/v5 v5.8.1; promoted google/go-cmp to direct require; removed obsolete kustomize/cmd/config.
Documentation updates for downloads and installation
README.md
Added Downloads section with links for Stable, main, and Nightly build artifacts; updated Install instructions to direct users to the new Downloads section.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

  • Add unit tests for the transform command crane#402: Extensive unit and integration tests added for the transform command's stage orchestration, validation, and resolution logic directly address the request to add comprehensive unit test coverage for the transform command.

Poem

🐰 From single stages grew a multi-stage dream,
Positional args flowing like a software stream,
Instructions files craft the perfect migration song,
Embedded kustomize makes the transform strong,
Cluster-scoped resources sorted just right—hop! 🥬

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sync/upstream-main-20260604

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

Test Coverage Report

Total: 46.5%

Per-package coverage

  • github.com/konveyor/crane — 0.0%
  • github.com/konveyor/crane/cmd/apply — 54.9%
  • github.com/konveyor/crane/cmd/convert — 0.0%
  • github.com/konveyor/crane/cmd/export — 93.1%
  • github.com/konveyor/crane/cmd/plugin-manager — 0.0%
  • github.com/konveyor/crane/cmd/plugin-manager/add — 0.0%
  • github.com/konveyor/crane/cmd/plugin-manager/list — 0.0%
  • github.com/konveyor/crane/cmd/plugin-manager/remove — 0.0%
  • github.com/konveyor/crane/cmd/skopeo-sync-gen — 0.0%
  • github.com/konveyor/crane/cmd/transfer-pvc — 9.2%
  • github.com/konveyor/crane/cmd/transform — 50.9%
  • github.com/konveyor/crane/cmd/transform/listplugins — 23.0%
  • github.com/konveyor/crane/cmd/transform/optionals — 0.0%
  • github.com/konveyor/crane/cmd/tunnel-api — 0.0%
  • github.com/konveyor/crane/cmd/validate — 39.1%
  • github.com/konveyor/crane/cmd/version — 20.0%
  • github.com/konveyor/crane/internal/apply — 33.6%
  • github.com/konveyor/crane/internal/buildinfo — 100.0%
  • github.com/konveyor/crane/internal/file — 71.4%
  • github.com/konveyor/crane/internal/flags — 0.0%
  • github.com/konveyor/crane/internal/kustomize — 85.7%
  • github.com/konveyor/crane/internal/plugin — 32.9%
  • github.com/konveyor/crane/internal/transform — 80.9%
  • github.com/konveyor/crane/internal/validate — 58.2%
Full function-level details
github.com/konveyor/crane/cmd/apply/apply.go:41:			Complete					100.0%
github.com/konveyor/crane/cmd/apply/apply.go:47:			Validate					100.0%
github.com/konveyor/crane/cmd/apply/apply.go:52:			Run						0.0%
github.com/konveyor/crane/cmd/apply/apply.go:56:			NewApplyCommand					0.0%
github.com/konveyor/crane/cmd/apply/apply.go:97:			getStageNames					100.0%
github.com/konveyor/crane/cmd/apply/apply.go:105:			addFlagsForOptions				0.0%
github.com/konveyor/crane/cmd/apply/apply.go:118:			run						84.2%
github.com/konveyor/crane/cmd/convert/convert.go:30:			NewConvertOptions				0.0%
github.com/konveyor/crane/cmd/convert/convert.go:63:			addFlagsForConvertOptions			0.0%
github.com/konveyor/crane/cmd/convert/convert.go:74:			Complete					0.0%
github.com/konveyor/crane/cmd/convert/convert.go:83:			Run						0.0%
github.com/konveyor/crane/cmd/convert/convert.go:87:			run						0.0%
github.com/konveyor/crane/cmd/convert/convert.go:112:			getClientFromContext				0.0%
github.com/konveyor/crane/cmd/convert/convert.go:134:			getRestConfigFromContext			0.0%
github.com/konveyor/crane/cmd/export/cluster.go:29:			NewClusterScopeHandler				100.0%
github.com/konveyor/crane/cmd/export/cluster.go:35:			isClusterScopedResource				100.0%
github.com/konveyor/crane/cmd/export/cluster.go:44:			filterRbacResources				100.0%
github.com/konveyor/crane/cmd/export/cluster.go:102:			NewClusterScopedRbacHandler			100.0%
github.com/konveyor/crane/cmd/export/cluster.go:113:			exportedSANamespaces				100.0%
github.com/konveyor/crane/cmd/export/cluster.go:126:			groupMatchesExportedSANamespaces		100.0%
github.com/konveyor/crane/cmd/export/cluster.go:143:			parseServiceAccountUserSubject			100.0%
github.com/konveyor/crane/cmd/export/cluster.go:154:			prepareForFiltering				100.0%
github.com/konveyor/crane/cmd/export/cluster.go:187:			filteredResourcesOfKind				100.0%
github.com/konveyor/crane/cmd/export/cluster.go:213:			accept						100.0%
github.com/konveyor/crane/cmd/export/cluster.go:223:			acceptClusterRoleBinding			100.0%
github.com/konveyor/crane/cmd/export/cluster.go:260:			acceptClusterRole				100.0%
github.com/konveyor/crane/cmd/export/cluster.go:284:			acceptSecurityContextConstraints		100.0%
github.com/konveyor/crane/cmd/export/cluster.go:336:			anyServiceAccountInNamespace			100.0%
github.com/konveyor/crane/cmd/export/crd.go:17:				normalizeGroupSet				85.7%
github.com/konveyor/crane/cmd/export/crd.go:29:				shouldSkipCRDGroup				100.0%
github.com/konveyor/crane/cmd/export/crd.go:50:				crdFailureAPIResourceName			100.0%
github.com/konveyor/crane/cmd/export/crd.go:56:				getOperatorManager				92.9%
github.com/konveyor/crane/cmd/export/crd.go:88:				collectRelatedCRDs				90.6%
github.com/konveyor/crane/cmd/export/discover.go:39:			hasClusterScopedManifests			100.0%
github.com/konveyor/crane/cmd/export/discover.go:57:			prepareClusterResourceDir			100.0%
github.com/konveyor/crane/cmd/export/discover.go:73:			prepareFailuresDir				100.0%
github.com/konveyor/crane/cmd/export/discover.go:85:			writeResources					85.7%
github.com/konveyor/crane/cmd/export/discover.go:133:			writeErrors					83.3%
github.com/konveyor/crane/cmd/export/discover.go:174:			getFilePath					100.0%
github.com/konveyor/crane/cmd/export/discover.go:185:			discoverPreferredResources			100.0%
github.com/konveyor/crane/cmd/export/discover.go:214:			resourceToExtract				100.0%
github.com/konveyor/crane/cmd/export/discover.go:283:			isAdmittedResource				100.0%
github.com/konveyor/crane/cmd/export/discover.go:292:			getObjects					100.0%
github.com/konveyor/crane/cmd/export/discover.go:326:			iterateItemsByGet				93.8%
github.com/konveyor/crane/cmd/export/discover.go:352:			iterateItemsInList				92.3%
github.com/konveyor/crane/cmd/export/export.go:52:			Complete					90.0%
github.com/konveyor/crane/cmd/export/export.go:91:			Validate					100.0%
github.com/konveyor/crane/cmd/export/export.go:106:			validateExportNamespace				87.5%
github.com/konveyor/crane/cmd/export/export.go:122:			Run						0.0%
github.com/konveyor/crane/cmd/export/export.go:225:			NewExportCommand				50.0%
github.com/konveyor/crane/cmd/plugin-manager/add/add.go:42:		Complete					0.0%
github.com/konveyor/crane/cmd/plugin-manager/add/add.go:47:		Validate					0.0%
github.com/konveyor/crane/cmd/plugin-manager/add/add.go:90:		Run						0.0%
github.com/konveyor/crane/cmd/plugin-manager/add/add.go:94:		NewAddCommand					0.0%
github.com/konveyor/crane/cmd/plugin-manager/add/add.go:126:		addFlagsForOptions				0.0%
github.com/konveyor/crane/cmd/plugin-manager/add/add.go:131:		run						0.0%
github.com/konveyor/crane/cmd/plugin-manager/add/add.go:195:		downloadBinary					0.0%
github.com/konveyor/crane/cmd/plugin-manager/list/list.go:46:		Complete					0.0%
github.com/konveyor/crane/cmd/plugin-manager/list/list.go:51:		Validate					0.0%
github.com/konveyor/crane/cmd/plugin-manager/list/list.go:56:		Run						0.0%
github.com/konveyor/crane/cmd/plugin-manager/list/list.go:60:		NewListCommand					0.0%
github.com/konveyor/crane/cmd/plugin-manager/list/list.go:90:		addFlagsForOptions				0.0%
github.com/konveyor/crane/cmd/plugin-manager/list/list.go:98:		run						0.0%
github.com/konveyor/crane/cmd/plugin-manager/list/list.go:167:		printInstalledInformation			0.0%
github.com/konveyor/crane/cmd/plugin-manager/list/list.go:177:		groupInformationForPlugins			0.0%
github.com/konveyor/crane/cmd/plugin-manager/list/list.go:196:		printInformation				0.0%
github.com/konveyor/crane/cmd/plugin-manager/list/list.go:208:		printParamsInformation				0.0%
github.com/konveyor/crane/cmd/plugin-manager/list/list.go:222:		getOptionalFields				0.0%
github.com/konveyor/crane/cmd/plugin-manager/list/list.go:247:		printTable					0.0%
github.com/konveyor/crane/cmd/plugin-manager/plugin-manager.go:33:	Complete					0.0%
github.com/konveyor/crane/cmd/plugin-manager/plugin-manager.go:38:	Validate					0.0%
github.com/konveyor/crane/cmd/plugin-manager/plugin-manager.go:43:	Run						0.0%
github.com/konveyor/crane/cmd/plugin-manager/plugin-manager.go:47:	NewPluginManagerCommand				0.0%
github.com/konveyor/crane/cmd/plugin-manager/plugin-manager.go:78:	addFlagsForOptions				0.0%
github.com/konveyor/crane/cmd/plugin-manager/plugin-manager.go:86:	run						0.0%
github.com/konveyor/crane/cmd/plugin-manager/remove/remove.go:33:	Complete					0.0%
github.com/konveyor/crane/cmd/plugin-manager/remove/remove.go:38:	Validate					0.0%
github.com/konveyor/crane/cmd/plugin-manager/remove/remove.go:43:	Run						0.0%
github.com/konveyor/crane/cmd/plugin-manager/remove/remove.go:47:	NewRemoveCommand				0.0%
github.com/konveyor/crane/cmd/plugin-manager/remove/remove.go:77:	run						0.0%
github.com/konveyor/crane/cmd/skopeo-sync-gen/skopeo-sync-gen.go:61:	Complete					0.0%
github.com/konveyor/crane/cmd/skopeo-sync-gen/skopeo-sync-gen.go:65:	Validate					0.0%
github.com/konveyor/crane/cmd/skopeo-sync-gen/skopeo-sync-gen.go:69:	NewSkopeoSyncGenCommand				0.0%
github.com/konveyor/crane/cmd/skopeo-sync-gen/skopeo-sync-gen.go:103:	shouldAddImageStream				0.0%
github.com/konveyor/crane/cmd/skopeo-sync-gen/skopeo-sync-gen.go:114:	Run						0.0%
github.com/konveyor/crane/cmd/transfer-pvc/progress.go:37:		NewRsyncLogStream				0.0%
github.com/konveyor/crane/cmd/transfer-pvc/progress.go:50:		Init						0.0%
github.com/konveyor/crane/cmd/transfer-pvc/progress.go:138:		writeProgressToFile				0.0%
github.com/konveyor/crane/cmd/transfer-pvc/progress.go:148:		Close						0.0%
github.com/konveyor/crane/cmd/transfer-pvc/progress.go:154:		Streams						0.0%
github.com/konveyor/crane/cmd/transfer-pvc/progress.go:190:		addDataSize					0.0%
github.com/konveyor/crane/cmd/transfer-pvc/progress.go:215:		String						100.0%
github.com/konveyor/crane/cmd/transfer-pvc/progress.go:219:		MarshalJSON					0.0%
github.com/konveyor/crane/cmd/transfer-pvc/progress.go:223:		AsString					0.0%
github.com/konveyor/crane/cmd/transfer-pvc/progress.go:271:		NewProgress					100.0%
github.com/konveyor/crane/cmd/transfer-pvc/progress.go:291:		Completed					0.0%
github.com/konveyor/crane/cmd/transfer-pvc/progress.go:296:		Status						38.5%
github.com/konveyor/crane/cmd/transfer-pvc/progress.go:321:		Merge						0.0%
github.com/konveyor/crane/cmd/transfer-pvc/progress.go:376:		newDataSize					72.7%
github.com/konveyor/crane/cmd/transfer-pvc/progress.go:399:		parseRsyncLogs					76.1%
github.com/konveyor/crane/cmd/transfer-pvc/progress.go:479:		waitForPodRunning				0.0%
github.com/konveyor/crane/cmd/transfer-pvc/progress.go:513:		getFinalPodStatus				0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:89:		Validate					0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:117:		Validate					0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:133:		NewTransferPVCCommand				0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:169:		addFlagsToTransferPVCCommand			0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:189:		Complete					0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:220:		Validate					0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:246:		Run						0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:250:		getClientFromContext				0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:271:		getRestConfigFromContext			0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:278:		run						0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:485:		getValidatedResourceName			0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:495:		getNodeNameForPVC				0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:515:		getIDsForNamespace				0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:541:		getRsyncClientPodSecurityContext		0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:553:		getRsyncServerPodSecurityContext		0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:565:		garbageCollect					0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:602:		deleteResourcesIteratively			0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:628:		deleteResourcesForGVK				0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:649:		followClientLogs				0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:678:		waitForEndpoint					0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:690:		createEndpoint					0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:735:		getRouteHostName				0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:752:		buildDestinationPVC				0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:773:		ApplyTo						0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:793:		ApplyTo						0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:805:		ApplyTo						0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:821:		String						0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:825:		Set						0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:835:		Type						0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:841:		parseSourceDestinationMapping			100.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:863:		String						0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:867:		Set						0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:876:		Type						0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:880:		String						0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:884:		Set						0.0%
github.com/konveyor/crane/cmd/transfer-pvc/transfer-pvc.go:894:		Type						0.0%
github.com/konveyor/crane/cmd/transform/listplugins/listplugins.go:33:	Complete					0.0%
github.com/konveyor/crane/cmd/transform/listplugins/listplugins.go:38:	Validate					0.0%
github.com/konveyor/crane/cmd/transform/listplugins/listplugins.go:43:	Run						0.0%
github.com/konveyor/crane/cmd/transform/listplugins/listplugins.go:47:	NewListPluginsCommand				0.0%
github.com/konveyor/crane/cmd/transform/listplugins/listplugins.go:80:	GetPluginNames					85.7%
github.com/konveyor/crane/cmd/transform/listplugins/listplugins.go:95:	getFilteredPlugins				75.0%
github.com/konveyor/crane/cmd/transform/listplugins/listplugins.go:104:	run						0.0%
github.com/konveyor/crane/cmd/transform/optionals/optionals.go:31:	Complete					0.0%
github.com/konveyor/crane/cmd/transform/optionals/optionals.go:36:	Validate					0.0%
github.com/konveyor/crane/cmd/transform/optionals/optionals.go:41:	Run						0.0%
github.com/konveyor/crane/cmd/transform/optionals/optionals.go:45:	NewOptionalsCommand				0.0%
github.com/konveyor/crane/cmd/transform/optionals/optionals.go:77:	run						0.0%
github.com/konveyor/crane/cmd/transform/transform.go:56:		Complete					0.0%
github.com/konveyor/crane/cmd/transform/transform.go:62:		Validate					0.0%
github.com/konveyor/crane/cmd/transform/transform.go:67:		Run						0.0%
github.com/konveyor/crane/cmd/transform/transform.go:72:		getPluginCompletions				100.0%
github.com/konveyor/crane/cmd/transform/transform.go:97:		NewTransformCommand				0.0%
github.com/konveyor/crane/cmd/transform/transform.go:140:		addFlagsForOptions				0.0%
github.com/konveyor/crane/cmd/transform/transform.go:159:		run						11.8%
github.com/konveyor/crane/cmd/transform/transform.go:310:		optionalFlagsToLower				0.0%
github.com/konveyor/crane/cmd/transform/transform.go:320:		runStageWithCleanup				0.0%
github.com/konveyor/crane/cmd/transform/transform.go:336:		reconcileInstructionStages			80.0%
github.com/konveyor/crane/cmd/transform/transform.go:385:		ensurePreviousStagesRun				83.3%
github.com/konveyor/crane/cmd/transform/transform.go:402:		ensureStagesHaveOutput				92.9%
github.com/konveyor/crane/cmd/transform/transform.go:441:		createDefaultStagesForAllPlugins		94.7%
github.com/konveyor/crane/cmd/transform/transform.go:493:		validateStageNameToken				100.0%
github.com/konveyor/crane/cmd/transform/transform.go:508:		findStageByDirName				100.0%
github.com/konveyor/crane/cmd/transform/transform.go:520:		findStagesByName				100.0%
github.com/konveyor/crane/cmd/transform/transform.go:532:		createStageDirectory				77.8%
github.com/konveyor/crane/cmd/transform/transform.go:553:		createCustomStageWithExplicitName		100.0%
github.com/konveyor/crane/cmd/transform/transform.go:569:		createCustomStageWithAutoPriority		71.4%
github.com/konveyor/crane/cmd/transform/transform.go:591:		createPluginStage				0.0%
github.com/konveyor/crane/cmd/transform/transform.go:624:		resolveAndValidateStages			57.3%
github.com/konveyor/crane/cmd/tunnel-api/tunnel-api.go:35:		NewTunnelAPIOptions				0.0%
github.com/konveyor/crane/cmd/tunnel-api/tunnel-api.go:65:		addFlagsForTunnelAPIOptions			0.0%
github.com/konveyor/crane/cmd/tunnel-api/tunnel-api.go:77:		Complete					0.0%
github.com/konveyor/crane/cmd/tunnel-api/tunnel-api.go:100:		Validate					0.0%
github.com/konveyor/crane/cmd/tunnel-api/tunnel-api.go:116:		Run						0.0%
github.com/konveyor/crane/cmd/tunnel-api/tunnel-api.go:120:		getClientFromContext				0.0%
github.com/konveyor/crane/cmd/tunnel-api/tunnel-api.go:129:		getRestConfigFromContext			0.0%
github.com/konveyor/crane/cmd/tunnel-api/tunnel-api.go:136:		run						0.0%
github.com/konveyor/crane/cmd/validate/validate.go:33:			Complete					0.0%
github.com/konveyor/crane/cmd/validate/validate.go:45:			Validate					100.0%
github.com/konveyor/crane/cmd/validate/validate.go:75:			Run						12.2%
github.com/konveyor/crane/cmd/validate/validate.go:155:			NewValidateCommand				44.4%
github.com/konveyor/crane/cmd/version/version.go:20:			Complete					0.0%
github.com/konveyor/crane/cmd/version/version.go:25:			Validate					0.0%
github.com/konveyor/crane/cmd/version/version.go:30:			Run						0.0%
github.com/konveyor/crane/cmd/version/version.go:34:			NewVersionCommand				0.0%
github.com/konveyor/crane/cmd/version/version.go:60:			run						100.0%
github.com/konveyor/crane/e2e-tests/utils/utils.go:20:			CreateTempDir					100.0%
github.com/konveyor/crane/e2e-tests/utils/utils.go:27:			ListFilesRecursively				91.7%
github.com/konveyor/crane/e2e-tests/utils/utils.go:50:			ListFilesRecursivelyAsList			80.0%
github.com/konveyor/crane/e2e-tests/utils/utils.go:74:			HasFilesRecursively				80.0%
github.com/konveyor/crane/e2e-tests/utils/utils.go:85:			ReadTestdataFile				88.9%
github.com/konveyor/crane/e2e-tests/utils/utils.go:104:			TestdataFilePath				0.0%
github.com/konveyor/crane/e2e-tests/utils/utils.go:124:			GoldenManifestsDir				90.0%
github.com/konveyor/crane/e2e-tests/utils/utils.go:143:			CompareDirectoryFileSets			78.9%
github.com/konveyor/crane/e2e-tests/utils/utils.go:178:			CompareDirectoryYAMLSemantics			82.4%
github.com/konveyor/crane/e2e-tests/utils/utils.go:212:			CompareDirectoryYAMLSemanticsExport		89.3%
github.com/konveyor/crane/e2e-tests/utils/utils.go:264:			buildNormalizedExportIndex			81.0%
github.com/konveyor/crane/e2e-tests/utils/utils.go:304:			canonicalizeDocs				87.5%
github.com/konveyor/crane/e2e-tests/utils/utils.go:318:			extractResourceIdentity				77.8%
github.com/konveyor/crane/e2e-tests/utils/utils.go:381:			parseYAMLDocuments				100.0%
github.com/konveyor/crane/e2e-tests/utils/utils.go:400:			compareYAMLFileBytes				100.0%
github.com/konveyor/crane/e2e-tests/utils/utils.go:420:			AssertNoKindsInOutput				0.0%
github.com/konveyor/crane/e2e-tests/utils/utils.go:476:			AssertKindsInOutput				0.0%
github.com/konveyor/crane/e2e-tests/utils/utils.go:528:			LooksLikeYAMLFile				100.0%
github.com/konveyor/crane/e2e-tests/utils/utils.go:542:			normalizeUnstableFields				100.0%
github.com/konveyor/crane/e2e-tests/utils/utils.go:579:			normalizePodServiceAccountVolumeNames		90.9%
github.com/konveyor/crane/e2e-tests/utils/utils.go:638:			normalizeWithPath				92.9%
github.com/konveyor/crane/e2e-tests/utils/utils.go:668:			shouldDropField					100.0%
github.com/konveyor/crane/e2e-tests/utils/utils.go:719:			AssertWhiteoutResourceFilesExist		0.0%
github.com/konveyor/crane/e2e-tests/utils/utils.go:759:			AssertWhiteoutResourceFileCount			0.0%
github.com/konveyor/crane/e2e-tests/utils/utils.go:791:			AssertWhiteoutCommentsInKustomization		0.0%
github.com/konveyor/crane/e2e-tests/utils/utils.go:846:			AssertKindsNotInActiveKustomizeResources	0.0%
github.com/konveyor/crane/e2e-tests/utils/utils.go:888:			CaptureAPISurfaceScriptPath			75.0%
github.com/konveyor/crane/internal/apply/kustomize.go:30:		ApplySingleStage				0.0%
github.com/konveyor/crane/internal/apply/kustomize.go:79:		ApplyMultiStage					0.0%
github.com/konveyor/crane/internal/apply/kustomize.go:133:		runKustomizeBuild				0.0%
github.com/konveyor/crane/internal/apply/kustomize.go:142:		filterClusterScopedResources			82.4%
github.com/konveyor/crane/internal/apply/kustomize.go:198:		splitMultiDocYAMLToFiles			85.7%
github.com/konveyor/crane/internal/buildinfo/buildinfo.go:25:		readKustomizeVersion				100.0%
github.com/konveyor/crane/internal/file/file_helper.go:22:		ReadFiles					100.0%
github.com/konveyor/crane/internal/file/file_helper.go:32:		readFiles					84.0%
github.com/konveyor/crane/internal/file/file_helper.go:83:		GetWhiteOutFilePath				100.0%
github.com/konveyor/crane/internal/file/file_helper.go:87:		GetTransformPath				100.0%
github.com/konveyor/crane/internal/file/file_helper.go:91:		GetIgnoredPatchesPath				0.0%
github.com/konveyor/crane/internal/file/file_helper.go:95:		updateTransformDirPath				100.0%
github.com/konveyor/crane/internal/file/file_helper.go:99:		updateIgnoredPatchesDirPath			0.0%
github.com/konveyor/crane/internal/file/file_helper.go:106:		updatePath					100.0%
github.com/konveyor/crane/internal/file/file_helper.go:113:		GetOutputFilePath				100.0%
github.com/konveyor/crane/internal/file/file_helper.go:123:		GetStageDir					100.0%
github.com/konveyor/crane/internal/file/file_helper.go:129:		GetResourcesDir					100.0%
github.com/konveyor/crane/internal/file/file_helper.go:135:		GetPatchesDir					100.0%
github.com/konveyor/crane/internal/file/file_helper.go:141:		GetReportsDir					100.0%
github.com/konveyor/crane/internal/file/file_helper.go:147:		GetWhiteoutsDir					100.0%
github.com/konveyor/crane/internal/file/file_helper.go:153:		GetKustomizationPath				100.0%
github.com/konveyor/crane/internal/file/file_helper.go:159:		GetMetadataPath					100.0%
github.com/konveyor/crane/internal/file/file_helper.go:165:		GetResourceTypeFilePath				100.0%
github.com/konveyor/crane/internal/file/file_helper.go:171:		GetPatchFilePath				100.0%
github.com/konveyor/crane/internal/file/file_helper.go:177:		GetWhiteoutReportPath				100.0%
github.com/konveyor/crane/internal/file/file_helper.go:183:		GetIgnoredPatchReportPath			100.0%
github.com/konveyor/crane/internal/file/file_helper.go:189:		GetStageWorkDir					0.0%
github.com/konveyor/crane/internal/file/file_helper.go:195:		GetStageInputDir				0.0%
github.com/konveyor/crane/internal/file/file_helper.go:202:		GetStageTransformDir				0.0%
github.com/konveyor/crane/internal/file/file_helper.go:208:		GetStageOutputDir				0.0%
github.com/konveyor/crane/internal/file/file_helper.go:215:		GetResourceFilename				0.0%
github.com/konveyor/crane/internal/flags/global_flags.go:14:		ApplyFlags					0.0%
github.com/konveyor/crane/internal/flags/global_flags.go:21:		GetLogger					0.0%
github.com/konveyor/crane/internal/flags/global_flags.go:29:		initConfig					0.0%
github.com/konveyor/crane/internal/kustomize/args.go:20:		ParseAndValidateArgs				100.0%
github.com/konveyor/crane/internal/kustomize/args.go:92:		splitArgs					100.0%
github.com/konveyor/crane/internal/kustomize/runner.go:21:		Build						82.4%
github.com/konveyor/crane/internal/kustomize/runner.go:55:		buildOptions					100.0%
github.com/konveyor/crane/internal/kustomize/runner.go:105:		setEnvVars					46.2%
github.com/konveyor/crane/internal/plugin/plugin_helper.go:21:		GetPlugins					0.0%
github.com/konveyor/crane/internal/plugin/plugin_helper.go:38:		getBinaryPlugins				0.0%
github.com/konveyor/crane/internal/plugin/plugin_helper.go:63:		IsExecAny					0.0%
github.com/konveyor/crane/internal/plugin/plugin_helper.go:67:		GetFilteredPlugins				0.0%
github.com/konveyor/crane/internal/plugin/plugin_helper.go:109:		isPluginInList					0.0%
github.com/konveyor/crane/internal/plugin/plugin_manager_helper.go:24:	BuildManifestMap				0.0%
github.com/konveyor/crane/internal/plugin/plugin_manager_helper.go:68:	GetYamlFromUrl					75.0%
github.com/konveyor/crane/internal/plugin/plugin_manager_helper.go:82:	YamlToManifest					72.7%
github.com/konveyor/crane/internal/plugin/plugin_manager_helper.go:104:	FilterPluginForOsArch				100.0%
github.com/konveyor/crane/internal/plugin/plugin_manager_helper.go:122:	GetDefaultSource				0.0%
github.com/konveyor/crane/internal/plugin/plugin_manager_helper.go:131:	LocateBinaryInPluginDir				0.0%
github.com/konveyor/crane/internal/plugin/plugin_manager_helper.go:143:	IsUrl						100.0%
github.com/konveyor/crane/internal/plugin/plugin_manager_helper.go:149:	getData						80.0%
github.com/konveyor/crane/internal/transform/instructions.go:31:	LoadInstructions				61.1%
github.com/konveyor/crane/internal/transform/instructions.go:63:	friendlyInstructionsDecodeError			87.5%
github.com/konveyor/crane/internal/transform/instructions.go:78:	ValidateInstructions				100.0%
github.com/konveyor/crane/internal/transform/instructions.go:112:	GenerateStageDirNames				100.0%
github.com/konveyor/crane/internal/transform/orchestrator.go:41:	RunMultiStage					75.0%
github.com/konveyor/crane/internal/transform/orchestrator.go:147:	executeStage					72.7%
github.com/konveyor/crane/internal/transform/orchestrator.go:199:	transformResources				68.4%
github.com/konveyor/crane/internal/transform/orchestrator.go:250:	formatResourceID				0.0%
github.com/konveyor/crane/internal/transform/orchestrator.go:257:	getPluginForStage				90.9%
github.com/konveyor/crane/internal/transform/orchestrator.go:284:	getAvailablePluginNames				0.0%
github.com/konveyor/crane/internal/transform/orchestrator.go:297:	applyStageTransforms				80.8%
github.com/konveyor/crane/internal/transform/orchestrator.go:354:	loadResourcesFromDirectory			85.7%
github.com/konveyor/crane/internal/transform/orchestrator.go:369:	writeResourcesToDirectory			73.1%
github.com/konveyor/crane/internal/transform/stages.go:22:		DiscoverStages					90.5%
github.com/konveyor/crane/internal/transform/stages.go:83:		FilterStages					100.0%
github.com/konveyor/crane/internal/transform/stages.go:112:		GetFirstStage					100.0%
github.com/konveyor/crane/internal/transform/stages.go:122:		GetLastStage					66.7%
github.com/konveyor/crane/internal/transform/stages.go:132:		GetPreviousStage				100.0%
github.com/konveyor/crane/internal/transform/stages.go:142:		GetNextStage					100.0%
github.com/konveyor/crane/internal/transform/stages.go:152:		ValidateStageName				100.0%
github.com/konveyor/crane/internal/transform/stages.go:161:		GenerateStageName				100.0%
github.com/konveyor/crane/internal/transform/test_helpers.go:8:		hasKustomizeCommand				100.0%
github.com/konveyor/crane/internal/transform/test_helpers.go:14:	contains					100.0%
github.com/konveyor/crane/internal/transform/test_helpers.go:23:	findInString					100.0%
github.com/konveyor/crane/internal/transform/writer.go:28:		NewKustomizeWriter				100.0%
github.com/konveyor/crane/internal/transform/writer.go:37:		WriteStage					68.4%
github.com/konveyor/crane/internal/transform/writer.go:218:		getResourceID					100.0%
github.com/konveyor/crane/internal/transform/writer.go:231:		filterValidRemoveOps				76.2%
github.com/konveyor/crane/internal/transform/writer.go:275:		pathExists					93.5%
github.com/konveyor/crane/internal/transform/writer.go:352:		generateKustomizationWithComments		93.8%
github.com/konveyor/crane/internal/transform/writer.go:383:		checkStageDirectory				23.1%
github.com/konveyor/crane/internal/validate/api_resources.go:23:	ParseAPIResourcesJSON				90.9%
github.com/konveyor/crane/internal/validate/matcher.go:25:		MatchResults					75.0%
github.com/konveyor/crane/internal/validate/matcher.go:36:		MatchResultsFromIndex				100.0%
github.com/konveyor/crane/internal/validate/matcher.go:72:		buildDiscoveryIndex				64.7%
github.com/konveyor/crane/internal/validate/matcher.go:102:		matchEntry					100.0%
github.com/konveyor/crane/internal/validate/matcher.go:131:		buildKindIndex					100.0%
github.com/konveyor/crane/internal/validate/matcher.go:143:		addSuggestion					92.3%
github.com/konveyor/crane/internal/validate/report.go:17:		FormatTable					91.3%
github.com/konveyor/crane/internal/validate/report.go:60:		FormatJSON					100.0%
github.com/konveyor/crane/internal/validate/report.go:67:		FormatYAML					0.0%
github.com/konveyor/crane/internal/validate/report.go:79:		WriteFailures					0.0%
github.com/konveyor/crane/internal/validate/report.go:115:		failureFileName					0.0%
github.com/konveyor/crane/internal/validate/report.go:129:		safeFilePart					0.0%
github.com/konveyor/crane/internal/validate/report.go:145:		parseAPIVersion					0.0%
github.com/konveyor/crane/internal/validate/scanner.go:34:		ScanManifests					75.4%
github.com/konveyor/crane/internal/validate/types.go:46:		HasIncompatible					100.0%
github.com/konveyor/crane/internal/validate/types.go:49:		IncompatibleResults				0.0%
github.com/konveyor/crane/main.go:16:					main						0.0%
total:									(statements)					46.5%

Posted by CI

@stillalearner stillalearner left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

lgtm

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/transform/writer.go (1)

216-226: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include API group/version in the resource identity key.

WriteStage() uses getResourceID() as the dedupe key for resources and whiteouts. With the current kind/namespace/name format, two resources that share a kind/name but come from different API groups or versions will overwrite each other in those maps, so one manifest can disappear from the generated stage output.

Suggested direction
 func getResourceID(resource unstructured.Unstructured) string {
+	gvk := resource.GroupVersionKind()
 	kind := resource.GetKind()
 	namespace := resource.GetNamespace()
 	name := resource.GetName()
 
 	if namespace != "" {
-		return fmt.Sprintf("%s/%s/%s", kind, namespace, name)
+		return fmt.Sprintf("%s/%s/%s/%s/%s", gvk.Group, gvk.Version, kind, namespace, name)
 	}
-	return fmt.Sprintf("%s/%s", kind, name)
+	return fmt.Sprintf("%s/%s/%s/%s", gvk.Group, gvk.Version, kind, name)
 }
🤖 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/transform/writer.go` around lines 216 - 226, getResourceID currently
returns kind/namespace/name which omits API group/version and causes
cross-group/version collisions; update getResourceID to include the resource API
version (Group/Version) by using resource.GetAPIVersion() (or
resource.GroupVersionKind() to build group/version) and return a key like
apiVersion/kind/namespace/name (or apiVersion/kind/name for cluster-scoped).
Modify the function getResourceID to prepend the apiVersion component to the
existing format so all dedupe maps in WriteStage() distinguish resources by API
group/version as well as kind/namespace/name.
internal/kustomize/args.go (1)

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

Validator accepts --enable-helm=<value> but the runner rejects it.

--enable-helm is a boolean flag, yet the whitelist check only compares argName (the part before =). An input like --enable-helm=true passes validation and is forwarded as a single token, but Runner.buildOptions matches only the exact string --enable-helm (and the --helm-command= prefix), so it falls through to the default branch and returns unsupported kustomize argument: "--enable-helm=true". The resulting error is confusing because the flag is on the whitelist.

Consider rejecting a value on boolean flags here so the error surfaces during validation with a clear message.

🤖 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/kustomize/args.go` around lines 64 - 67, The validator currently
only checks argName against AllowedKustomizeArgs and accepts tokens like
"--enable-helm=true" even though Runner.buildOptions expects the bare boolean
flag "--enable-helm"; update the validation in the arg handling block (the code
that uses argName and AllowedKustomizeArgs) to detect tokens containing '=' for
known boolean flags and reject them with a clear error, e.g. if argName is a
boolean-typed allowed key (like "--enable-helm") and the original token contains
'=', return an error asking to use the flag without a value; reference the
argName variable and AllowedKustomizeArgs map when implementing this check and
ensure Runner.buildOptions remains unchanged.
🧹 Nitpick comments (9)
README.md (1)

52-53: ⚡ Quick win

GitHub Actions artifact links require authentication and expire after 30 days.

The main and Nightly download links point to GitHub Actions workflow runs. Users must be authenticated to GitHub to download these artifacts, and the workflow sets retention-days: 30 (line 70 in the workflow file), so artifacts expire monthly. This differs significantly from the Stable release experience.

Consider adding a note to set user expectations or providing alternative hosting for unsigned development builds.

📝 Suggested clarification
 | Channel | Purpose | Link |
 | :-- | :-- | :-- |
 | **✅ Stable** | Tagged releases for production use | [GitHub Releases](https://github.com/migtools/crane/releases) |
-| **🛠️ `main`** | Latest artifacts from pushes to `main` | [Build Crane Binaries (main push)](https://github.com/migtools/crane/actions/workflows/build-release-binaries.yml?query=branch%3Amain+event%3Apush) |
-| **🌙 Nightly** | Scheduled artifacts for early validation | [Build Crane Binaries (scheduled)](https://github.com/migtools/crane/actions/workflows/build-release-binaries.yml?query=event%3Aschedule) |
+| **🛠️ `main`** | Latest artifacts from pushes to `main` (requires GitHub login; 30-day retention) | [Build Crane Binaries (main push)](https://github.com/migtools/crane/actions/workflows/build-release-binaries.yml?query=branch%3Amain+event%3Apush) |
+| **🌙 Nightly** | Scheduled artifacts for early validation (requires GitHub login; 30-day retention) | [Build Crane Binaries (scheduled)](https://github.com/migtools/crane/actions/workflows/build-release-binaries.yml?query=event%3Aschedule) |
🤖 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 `@README.md` around lines 52 - 53, The README's table entries for the "main"
and "Nightly" artifacts link to GitHub Actions workflow runs but don't warn
readers that those artifacts require GitHub authentication and expire after 30
days (the workflows set retention-days: 30); update the README near the **🛠️
`main`** and **🌙 Nightly** rows to add a short note that these links point to
authenticated, time-limited GitHub Actions artifacts (expires 30 days) and
either suggest users sign into GitHub to download or provide/mention an
alternate longer-term hosting option for unsigned development builds.
.github/workflows/build-release-binaries.yml (2)

57-58: ⚡ Quick win

Missing version and commit metadata in binaries.

The build step does not inject version or commit information via ldflags, unlike the Dockerfile build which sets MTA_OPS_VERSION and MTA_OPS_GIT_COMMIT. Users of these binaries won't be able to identify which version or commit they're running.

📦 Proposed fix to add version metadata
       binary="crane_${GOOS}_${GOARCH}${ext}"
-      go build -trimpath -o "dist/${binary}" .
+      VERSION="${{ github.ref_name }}"
+      if [ "${{ github.ref_type }}" != "tag" ]; then
+        VERSION="${{ github.sha }}"
+      fi
+      go build -trimpath \
+        -ldflags="-X github.com/konveyor/crane/internal/buildinfo.Version=${VERSION} -X github.com/konveyor/crane/internal/buildinfo.BuildCommit=${{ github.sha }}" \
+        -o "dist/${binary}" .

Based on learnings: konflux.Dockerfile (lines 20-35) demonstrates the repository convention of embedding MTA_OPS_VERSION and MTA_OPS_GIT_COMMIT via ldflags during multi-platform builds.

🤖 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 @.github/workflows/build-release-binaries.yml around lines 57 - 58, The go
build step that produces "dist/${binary}" currently omits embedding version and
commit metadata; update the build invocation (the go build line that outputs to
dist/${binary}) to pass ldflags that set the MTA_OPS_VERSION and
MTA_OPS_GIT_COMMIT variables (using the pipeline's version and commit values,
e.g. from a VERSION input or GITHUB_SHA) so the compiled binary contains those
symbols; ensure the workflow exposes/sets the version and commit env vars before
the build and uses them in the ldflags passed to go build.

59-62: ⚡ Quick win

Inefficient checksum file handling creates then deletes individual checksums.

The build step creates individual .sha256 files (line 61), the release job then recreates a combined checksums.txt (lines 88-97), and finally deletes the individual .sha256 files (line 100). This wastes CI time and complicates the artifact flow.

♻️ Proposed fix to skip individual checksum files

Remove individual checksum creation from the build step:

       binary="crane_${GOOS}_${GOARCH}${ext}"
       go build -trimpath -o "dist/${binary}" .
-      (
-        cd dist
-        sha256sum "${binary}" > "${binary}.sha256"
-      )

Then remove the cleanup step from the release job since there are no .sha256 files to delete:

-    - name: Keep only binaries and combined checksums file
-      run: rm -f release-assets/*.sha256
-
     - name: Create draft GitHub release

Also applies to: 99-100

🤖 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 @.github/workflows/build-release-binaries.yml around lines 59 - 62, Remove
the per-binary checksum creation to avoid redundant work: delete the sha256sum
"${binary}" > "${binary}.sha256" invocation in the build step (the subshell that
cd's into dist and emits "${binary}.sha256"), and also remove the cleanup step
in the release job that deletes individual .sha256 files (the rm -f
dist/*.sha256 or similar) since the release already generates a combined
checksums.txt; ensure only the combined checksums generation remains.
internal/buildinfo/buildinfo_test.go (1)

8-57: ⚡ Quick win

Convert these cases to a table-driven test.

These three tests exercise one function across the same setup/teardown pattern. Folding them into a single table-driven test will remove the repeated readBuildInfo override logic and matches the repo’s test convention for multi-scenario coverage. As per coding guidelines, "Use table-driven tests for multiple scenarios".

🤖 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/buildinfo/buildinfo_test.go` around lines 8 - 57, Replace the three
separate tests with a single table-driven test that iterates scenarios and sets
readBuildInfo per case; save the original readBuildInfo once into a variable
(original := readBuildInfo) and defer restoring it (defer func() { readBuildInfo
= original }()), then define a slice of test cases with fields name,
buildInfoReturn (a func returning *debug.BuildInfo, bool) and expected string,
loop over cases using t.Run(tc.name, func(t *testing.T) { readBuildInfo =
tc.buildInfoReturn(); got := readKustomizeVersion(); if got != tc.expected {
t.Errorf(...) } }), and include the same three scenarios (build info
unavailable, dep not found, dep found with "sigs.k8s.io/kustomize/kustomize/v5"
=> "v5.8.1") to remove duplicated setup/teardown while keeping the original
assertions.
cmd/transform/listplugins/listplugins.go (1)

95-101: ⚡ Quick win

Wrap plugin discovery errors with the directory being processed.

These raw returns drop the failing pluginDir, which makes list-plugins and completion failures much harder to debug once the path has already been normalized.

Proposed change
 func getFilteredPlugins(pluginDir string, skipPlugins []string, log *logrus.Logger) ([]cranelib.Plugin, error) {
 	absPluginDir, err := filepath.Abs(pluginDir)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("resolve plugin-dir %q to absolute path: %w", pluginDir, err)
 	}
 
-	return plugin.GetFilteredPlugins(absPluginDir, skipPlugins, log)
+	plugins, err := plugin.GetFilteredPlugins(absPluginDir, skipPlugins, log)
+	if err != nil {
+		return nil, fmt.Errorf("load plugins from %q: %w", absPluginDir, err)
+	}
+	return plugins, nil
 }

As per coding guidelines, "Prefer explicit error messages with context in Go code".

🤖 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/transform/listplugins/listplugins.go` around lines 95 - 101, The
getFilteredPlugins function currently returns raw errors from filepath.Abs and
plugin.GetFilteredPlugins without context; wrap both errors to include the
absolute plugin directory being processed (absPluginDir) so callers see which
path failed. Specifically, update getFilteredPlugins to return fmt.Errorf or
errors.Wrap-style messages that include absPluginDir when filepath.Abs fails and
when plugin.GetFilteredPlugins returns an error (referencing getFilteredPlugins
and plugin.GetFilteredPlugins to locate the spots), preserving the original
error as the cause.
cmd/transform/completion_test.go (1)

141-143: ⚡ Quick win

Remove dead logger initialization in cmd/transform/completion_test.go

log := logrus.New() is only used to call log.SetOutput(os.Stderr) and the resulting log is never referenced afterward; remove those lines and drop the logrus import if it becomes unused.

Proposed change
-			log := logrus.New()
-			log.SetOutput(os.Stderr)
-
 			completionFunc := getPluginCompletions(f)
🤖 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/transform/completion_test.go` around lines 141 - 143, Remove the dead
logger creation: delete the local variable assignment "log := logrus.New()" and
the subsequent "log.SetOutput(os.Stderr)" since the "log" variable is never
used; also remove the "logrus" import if it becomes unused after this change so
the test compiles cleanly, leaving only "f := &flags.GlobalFlags{}" as before.
cmd/apply/apply.go (1)

154-194: 💤 Low value

Stage-matching logic is duplicated with FilterStages.

The resolution loop here re-implements the exact stage.DirName == requested || stage.PluginName == requested matching that internalTransform.FilterStages already performs (see internal/transform/stages.go:74-109), then hands RequestedStages back to the selector so FilterStages matches a second time inside ApplyMultiStage. Consider exposing a small helper (e.g., a ResolveStages/validation function) in the transform package and reusing it, so the matching contract lives in one place and can't drift between apply and the orchestrator.

🤖 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/apply/apply.go` around lines 154 - 194, The code duplicates
stage-matching logic already implemented in internalTransform.FilterStages when
validating o.RequestedStages before building the StageSelector; refactor to
avoid drift by adding and using a single resolver/validator in the transform
package (e.g., expose internalTransform.ResolveStages or
ValidateRequestedStages) that takes existingStages (from
internalTransform.DiscoverStages) and the slice o.RequestedStages and returns
the resolved list and any unresolved names (or an error), then replace the
inline loop in the apply logic to call that new helper and build the
StageSelector using the resolved stage names (keep StageSelector and
ApplyMultiStage behavior unchanged so FilterStages still receives canonical
input).
internal/kustomize/runner_test.go (1)

250-325: 💤 Low value

Consider adding a Runner.Build test that exercises the --env restore path.

TestBuildOptions_ArgMapping validates parsing but never runs a build with --env, so setEnvVars/restoreEnv (the global-state mutation discussed in runner.go) is untested. A small test asserting that a pre-existing env var is restored (and a previously-unset key is unset) after Build would lock in that behavior.

🤖 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/kustomize/runner_test.go` around lines 250 - 325, Add a new unit
test that calls Runner.Build with the --env flag to exercise
setEnvVars/restoreEnv: create a Runner with Args containing "--env" and an env
pair (e.g., "FOO=bar"), set up a pre-existing environment key and an unset key,
run runner.Build(ctx) and then assert (1) the pre-existing key is restored to
its original value after Build and (2) the previously-unset key is unset again;
use the existing TestBuildOptions_ArgMapping patterns and reference
Runner.Build, Runner.buildOptions, setEnvVars, and restoreEnv to locate the
implementation to test.
cmd/apply/apply_test.go (1)

90-114: 💤 Low value

TestStageSelectionRouting re-implements the routing instead of asserting on run().

The test recomputes hasCustomSelector := len(tt.requestedStages) > 0 and compares lengths/indices locally; it never invokes the actual run()/selector construction. It will keep passing even if the real routing in run() regresses. Since TestRun_UnresolvedStagesError already drives the real path, consider folding selector-shape assertions into that flow (or extracting the selector-building into a testable helper) to make this test meaningful.

🤖 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/apply/apply_test.go` around lines 90 - 114, TestStageSelectionRouting
currently re-implements the selector logic instead of exercising the real code
path; change the test to call the actual run() path (which uses
ApplyMultiStage()) or refactor the selector construction into a new helper
(e.g., BuildStageSelector or ConstructSelector) that run() calls, then assert
the returned selector shape against tt.selectorStages; ensure
TestRun_UnresolvedStagesError is either extended to include these selector-shape
assertions or the new helper is unit-tested directly so the test no longer
compares len(tt.requestedStages) > 0 locally but validates the selector produced
by run() or the extracted helper.
🤖 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 @.github/workflows/build-release-binaries.yml:
- Line 37: Replace the mutable GitHub Actions tags with the provided commit SHAs
to avoid CI supply-chain risks: update uses: actions/checkout@v4 to uses:
actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5, actions/setup-go@v6
to actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c,
actions/upload-artifact@v4 to
actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02,
actions/download-artifact@v4 to
actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093, and
softprops/action-gh-release@v2 to
softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 in the
workflow file so each uses: entry references the exact commit SHA.

In `@cmd/transform/transform_test.go`:
- Around line 760-791: The test currently accepts any error from
o.resolveAndValidateStages, so change the assertions to require that err is
non-nil and specifically indicates the previous-stage-output failure: assert err
!= nil and assert the error message contains either "10_KubernetesPlugin" or the
words "previous" or "output" (or use errors.Is with a sentinel like
ErrPreviousStageNoOutput if one exists). Update the failure branch to call
t.Fatalf or t.Errorf if the error does not include those tokens so the test only
passes when ensurePreviousStagesRun reports the expected previous-stage output
error for the "50_CustomModifications" -> "10_KubernetesPlugin" scenario.

In `@cmd/transform/transform.go`:
- Around line 449-483: The default plugin stage priority spacing is
inconsistent: in the loop that creates default stages (using GenerateStageName,
createStageDirectory, and the local variable priority) it currently increments
priority by 5 which conflicts with GenerateStageDirNames() and ad-hoc creation
that use 10-step spacing; update the loop to advance priority by 10 (leave the
initial priority = 10) and adjust the comment to reflect 10-step increments so
generated stage names match other paths and reconcileInstructionStages() won’t
treat them as extras.

In `@e2e-tests/tests/mta_813_cronJob_PVC_test.go`:
- Around line 225-276: The test currently disables the PVC data-integrity
assertion so it no longer verifies transfer-pvc preserved source data; either
mark the whole spec pending/skip or restore a non-flaky check: re-enable the
pvc-reader flow (creating pod "pvc-reader" via scenario.KubectlTgtNonAdmin.Run
using pvcName/namespace), but make the assertion robust by waiting for the pod
to reach a terminal phase with a longer Eventually timeout and retrying logs
until they contain expectedLogSubstring (use Eventually around
scenario.KubectlTgtNonAdmin.Run("logs", "pvc-reader", "-n", namespace) and
assert it Contains expectedLogSubstring), or if flakiness persists, add a Ginkgo
Skip at the top of the spec referencing crane#408 until that bug is fixed.

In `@e2e-tests/utils/utils_test.go`:
- Around line 312-331: Add direct unit tests for the TestdataFilePath helper:
create a new table-driven test in e2e-tests/utils/utils_test.go (alongside
TestCaptureAPISurfaceScriptPath) that exercises TestdataFilePath() for the happy
path (valid filename returns an absolute, existing path ending with the expected
suffix) and the error path (empty filename returns the expected error).
Reference the TestdataFilePath function name to locate the helper, assert
filepath.IsAbs, strings.HasSuffix on filepath.Clean(result), and that os.Stat
succeeds for the valid case, and assert a non-nil/expected error for the
empty-filename case.

In `@e2e-tests/utils/utils.go`:
- Around line 104-115: runtime.Caller(0) can return a trimmed/relative filename,
so update TestdataFilePath (and likewise CaptureAPISurfaceScriptPath) to resolve
absolute filesystem paths: after obtaining thisFile from runtime.Caller(0) call
filepath.Abs(thisFile) (handle the error), use filepath.Dir on that absolute
path, build the final path with filepath.Join, then call filepath.Abs on the
final path (or return the already absolute result) and propagate any errors;
ensure both functions return an absolute path and proper error on failure.

In `@internal/transform/stages.go`:
- Around line 92-108: FilterStages currently returns matches in selector.Stages
order which breaks RunMultiStage's dependency chain; change FilterStages to
iterate over allStages and append a stage only if its DirName or PluginName is
present in selector.Stages (use a lookup set built from selector.Stages) while
still skipping duplicates via seen, so the returned filtered slice preserves the
pipeline order as defined by allStages; update references inside FilterStages to
use selector.Stages lookup, seen, filtered, allStages, stage.DirName and
stage.PluginName accordingly.

In `@internal/transform/writer_integration_test.go`:
- Around line 545-547: The test currently skips when kubectl/oc are missing via
hasKustomizeCommand(t), but applyStageTransforms() now uses the in-process
kustomize runner so remove that external-gate: delete the conditional skip block
that calls hasKustomizeCommand(t) in writer_integration_test.go and any other
identical checks around the 630-639 region so the embedded-runner regression
test always runs; keep the rest of the test intact (it exercises
applyStageTransforms()) and ensure no remaining references to
hasKustomizeCommand affect this test.

---

Outside diff comments:
In `@internal/kustomize/args.go`:
- Around line 64-67: The validator currently only checks argName against
AllowedKustomizeArgs and accepts tokens like "--enable-helm=true" even though
Runner.buildOptions expects the bare boolean flag "--enable-helm"; update the
validation in the arg handling block (the code that uses argName and
AllowedKustomizeArgs) to detect tokens containing '=' for known boolean flags
and reject them with a clear error, e.g. if argName is a boolean-typed allowed
key (like "--enable-helm") and the original token contains '=', return an error
asking to use the flag without a value; reference the argName variable and
AllowedKustomizeArgs map when implementing this check and ensure
Runner.buildOptions remains unchanged.

In `@internal/transform/writer.go`:
- Around line 216-226: getResourceID currently returns kind/namespace/name which
omits API group/version and causes cross-group/version collisions; update
getResourceID to include the resource API version (Group/Version) by using
resource.GetAPIVersion() (or resource.GroupVersionKind() to build group/version)
and return a key like apiVersion/kind/namespace/name (or apiVersion/kind/name
for cluster-scoped). Modify the function getResourceID to prepend the apiVersion
component to the existing format so all dedupe maps in WriteStage() distinguish
resources by API group/version as well as kind/namespace/name.

---

Nitpick comments:
In @.github/workflows/build-release-binaries.yml:
- Around line 57-58: The go build step that produces "dist/${binary}" currently
omits embedding version and commit metadata; update the build invocation (the go
build line that outputs to dist/${binary}) to pass ldflags that set the
MTA_OPS_VERSION and MTA_OPS_GIT_COMMIT variables (using the pipeline's version
and commit values, e.g. from a VERSION input or GITHUB_SHA) so the compiled
binary contains those symbols; ensure the workflow exposes/sets the version and
commit env vars before the build and uses them in the ldflags passed to go
build.
- Around line 59-62: Remove the per-binary checksum creation to avoid redundant
work: delete the sha256sum "${binary}" > "${binary}.sha256" invocation in the
build step (the subshell that cd's into dist and emits "${binary}.sha256"), and
also remove the cleanup step in the release job that deletes individual .sha256
files (the rm -f dist/*.sha256 or similar) since the release already generates a
combined checksums.txt; ensure only the combined checksums generation remains.

In `@cmd/apply/apply_test.go`:
- Around line 90-114: TestStageSelectionRouting currently re-implements the
selector logic instead of exercising the real code path; change the test to call
the actual run() path (which uses ApplyMultiStage()) or refactor the selector
construction into a new helper (e.g., BuildStageSelector or ConstructSelector)
that run() calls, then assert the returned selector shape against
tt.selectorStages; ensure TestRun_UnresolvedStagesError is either extended to
include these selector-shape assertions or the new helper is unit-tested
directly so the test no longer compares len(tt.requestedStages) > 0 locally but
validates the selector produced by run() or the extracted helper.

In `@cmd/apply/apply.go`:
- Around line 154-194: The code duplicates stage-matching logic already
implemented in internalTransform.FilterStages when validating o.RequestedStages
before building the StageSelector; refactor to avoid drift by adding and using a
single resolver/validator in the transform package (e.g., expose
internalTransform.ResolveStages or ValidateRequestedStages) that takes
existingStages (from internalTransform.DiscoverStages) and the slice
o.RequestedStages and returns the resolved list and any unresolved names (or an
error), then replace the inline loop in the apply logic to call that new helper
and build the StageSelector using the resolved stage names (keep StageSelector
and ApplyMultiStage behavior unchanged so FilterStages still receives canonical
input).

In `@cmd/transform/completion_test.go`:
- Around line 141-143: Remove the dead logger creation: delete the local
variable assignment "log := logrus.New()" and the subsequent
"log.SetOutput(os.Stderr)" since the "log" variable is never used; also remove
the "logrus" import if it becomes unused after this change so the test compiles
cleanly, leaving only "f := &flags.GlobalFlags{}" as before.

In `@cmd/transform/listplugins/listplugins.go`:
- Around line 95-101: The getFilteredPlugins function currently returns raw
errors from filepath.Abs and plugin.GetFilteredPlugins without context; wrap
both errors to include the absolute plugin directory being processed
(absPluginDir) so callers see which path failed. Specifically, update
getFilteredPlugins to return fmt.Errorf or errors.Wrap-style messages that
include absPluginDir when filepath.Abs fails and when plugin.GetFilteredPlugins
returns an error (referencing getFilteredPlugins and plugin.GetFilteredPlugins
to locate the spots), preserving the original error as the cause.

In `@internal/buildinfo/buildinfo_test.go`:
- Around line 8-57: Replace the three separate tests with a single table-driven
test that iterates scenarios and sets readBuildInfo per case; save the original
readBuildInfo once into a variable (original := readBuildInfo) and defer
restoring it (defer func() { readBuildInfo = original }()), then define a slice
of test cases with fields name, buildInfoReturn (a func returning
*debug.BuildInfo, bool) and expected string, loop over cases using
t.Run(tc.name, func(t *testing.T) { readBuildInfo = tc.buildInfoReturn(); got :=
readKustomizeVersion(); if got != tc.expected { t.Errorf(...) } }), and include
the same three scenarios (build info unavailable, dep not found, dep found with
"sigs.k8s.io/kustomize/kustomize/v5" => "v5.8.1") to remove duplicated
setup/teardown while keeping the original assertions.

In `@internal/kustomize/runner_test.go`:
- Around line 250-325: Add a new unit test that calls Runner.Build with the
--env flag to exercise setEnvVars/restoreEnv: create a Runner with Args
containing "--env" and an env pair (e.g., "FOO=bar"), set up a pre-existing
environment key and an unset key, run runner.Build(ctx) and then assert (1) the
pre-existing key is restored to its original value after Build and (2) the
previously-unset key is unset again; use the existing
TestBuildOptions_ArgMapping patterns and reference Runner.Build,
Runner.buildOptions, setEnvVars, and restoreEnv to locate the implementation to
test.

In `@README.md`:
- Around line 52-53: The README's table entries for the "main" and "Nightly"
artifacts link to GitHub Actions workflow runs but don't warn readers that those
artifacts require GitHub authentication and expire after 30 days (the workflows
set retention-days: 30); update the README near the **🛠️ `main`** and **🌙
Nightly** rows to add a short note that these links point to authenticated,
time-limited GitHub Actions artifacts (expires 30 days) and either suggest users
sign into GitHub to download or provide/mention an alternate longer-term hosting
option for unsigned development builds.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2de6a1fc-992a-4e42-9039-f09c489423c9

📥 Commits

Reviewing files that changed from the base of the PR and between 74558ec and 2de42df.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (45)
  • .github/workflows/build-release-binaries.yml
  • README.md
  • cmd/apply/apply.go
  • cmd/apply/apply_test.go
  • cmd/transform/completion_test.go
  • cmd/transform/listplugins/listplugins.go
  • cmd/transform/listplugins/listplugins_test.go
  • cmd/transform/transform.go
  • cmd/transform/transform_test.go
  • cmd/validate/validate.go
  • cmd/validate/validate_test.go
  • cmd/version/version.go
  • cmd/version/version_test.go
  • e2e-tests/framework/crane.go
  • e2e-tests/framework/scenario.go
  • e2e-tests/testdata/basic-instructions-file.yaml
  • e2e-tests/tests/mta_813_cronJob_PVC_test.go
  • e2e-tests/tests/mta_827_custom_transformation_stage_test.go
  • e2e-tests/tests/mta_828_instructions_file_migration_test.go
  • e2e-tests/tests/mta_829_validate_alternative_gv_suggestion_test.go
  • e2e-tests/tests/mta_830_instructions_file_force_reconcile_test.go
  • e2e-tests/tests/mta_832_validate_alternative_gv_suggestion_offline_test.go
  • e2e-tests/utils/utils.go
  • e2e-tests/utils/utils_test.go
  • go.mod
  • internal/apply/kustomize.go
  • internal/apply/kustomize_test.go
  • internal/buildinfo/buildinfo.go
  • internal/buildinfo/buildinfo_test.go
  • internal/file/file_helper.go
  • internal/kustomize/args.go
  • internal/kustomize/args_test.go
  • internal/kustomize/runner.go
  • internal/kustomize/runner_test.go
  • internal/plugin/plugin_helper.go
  • internal/transform/instructions.go
  • internal/transform/instructions_test.go
  • internal/transform/orchestrator.go
  • internal/transform/orchestrator_test.go
  • internal/transform/stages.go
  • internal/transform/stages_test.go
  • internal/transform/test_helpers.go
  • internal/transform/writer.go
  • internal/transform/writer_integration_test.go
  • internal/transform/writer_test.go
💤 Files with no reviewable changes (1)
  • internal/file/file_helper.go


steps:
- name: Checkout
uses: actions/checkout@v4

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 | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch current commit SHAs for all actions used in the workflow

echo "actions/checkout@v4:"
gh api repos/actions/checkout/commits/v4 --jq '.sha'

echo "actions/setup-go@v6:"
gh api repos/actions/setup-go/commits/v6 --jq '.sha'

echo "actions/upload-artifact@v4:"
gh api repos/actions/upload-artifact/commits/v4 --jq '.sha'

echo "actions/download-artifact@v4:"
gh api repos/actions/download-artifact/commits/v4 --jq '.sha'

echo "softprops/action-gh-release@v2:"
gh api repos/softprops/action-gh-release/commits/v2 --jq '.sha'

Repository: migtools/mta-crane

Length of output: 399


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/build-release-binaries.yml"

echo "== Target file =="
ls -la "$FILE"
echo

echo "== uses: lines (with context) =="
rg -n "^\s*uses:" "$FILE" -n --context 2 || true

echo
echo "== Lines 30-110 (for the referenced ranges) =="
nl -ba "$FILE" | sed -n '30,110p'

Repository: migtools/mta-crane

Length of output: 1010


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/build-release-binaries.yml"

echo "== All uses: entries =="
rg -n "^\s*uses:" "$FILE" || true

echo
echo "== Exact lines 30-110 =="
cat -n "$FILE" | sed -n '30,110p'

Repository: migtools/mta-crane

Length of output: 3017


Unpinned GitHub Actions introduce CI supply-chain risk.

This workflow uses mutable tags; pin them to commit SHAs:

  • actions/checkout@v4 (line 37) → 34e114876b0b11c390a56381ad16ebd13914f8d5
  • actions/setup-go@v6 (lines 40-42) → 4a3601121dd01d1626a1e23e37211e3254c1c06c
  • actions/upload-artifact@v4 (line 65) → ea165f8d65b6e75b540449e92b4886f43607fa02
  • actions/download-artifact@v4 (line 82) → d3f86a106a0bac45b974a628896c90dbdf5c8093
  • softprops/action-gh-release@v2 (line 103) → 3bb12739c298aeb8a4eeaf626c5b8d85266b0e65
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 36-37: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 37-37: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 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 @.github/workflows/build-release-binaries.yml at line 37, Replace the mutable
GitHub Actions tags with the provided commit SHAs to avoid CI supply-chain
risks: update uses: actions/checkout@v4 to uses:
actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5, actions/setup-go@v6
to actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c,
actions/upload-artifact@v4 to
actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02,
actions/download-artifact@v4 to
actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093, and
softprops/action-gh-release@v2 to
softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 in the
workflow file so each uses: entry references the exact commit SHA.

Comment on lines +760 to +791
// Configure orchestrator with minimal setup needed for ensurePreviousStagesRun
orchestrator := &internalTransform.Orchestrator{
Log: log,
ExportDir: exportDir,
TransformDir: transformDir,
PluginDir: pluginDir,
NewlyCreatedStages: make(map[string]bool),
}

// Request a custom stage
// This should fail because the previous stage (10_KubernetesPlugin) has no output
_, err := o.resolveAndValidateStages(
[]string{"50_CustomModifications"},
orchestrator,
transformDir,
pluginDir,
log,
)

// We expect this to fail because ensurePreviousStagesRun will try to run
// the previous stage which will fail due to missing export/plugin setup
if err == nil {
t.Errorf("expected error when previous stage has no output, but got none")
}

// The error should mention the previous stage or output directory
if err != nil && !strings.Contains(err.Error(), "10_KubernetesPlugin") &&
!strings.Contains(err.Error(), "output") &&
!strings.Contains(err.Error(), "previous") {
t.Logf("Got error (which is expected): %v", err)
// This is acceptable - the important thing is that it tried to ensure previous stages
}

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 | 🟡 Minor | ⚡ Quick win

Make this assert the previous-stage-output failure specifically.

Right now the test passes on any error, including unrelated setup failures from the intentionally incomplete orchestrator. That means it won't reliably catch a regression in the previous-stage-output check itself.

Based on learnings, "Bug fixes should include regression tests 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/transform/transform_test.go` around lines 760 - 791, The test currently
accepts any error from o.resolveAndValidateStages, so change the assertions to
require that err is non-nil and specifically indicates the previous-stage-output
failure: assert err != nil and assert the error message contains either
"10_KubernetesPlugin" or the words "previous" or "output" (or use errors.Is with
a sentinel like ErrPreviousStageNoOutput if one exists). Update the failure
branch to call t.Fatalf or t.Errorf if the error does not include those tokens
so the test only passes when ensurePreviousStagesRun reports the expected
previous-stage output error for the "50_CustomModifications" ->
"10_KubernetesPlugin" scenario.

Comment on lines +449 to +483
// Sort plugins by name for deterministic ordering
sort.Slice(allPlugins, func(i, j int) bool {
return allPlugins[i].Metadata().Name < allPlugins[j].Metadata().Name
})

// Assign priority to each plugin
// Start at 10, increment by 5 for each plugin
priority := 10

for _, plugin := range allPlugins {
pluginName := plugin.Metadata().Name

// Validate plugin name is safe to use as directory name
if err := validateStageNameToken(pluginName); err != nil {
log.Warnf("Skipping plugin %q: %v", pluginName, err)
continue
}

// Require "Plugin" suffix
if !strings.HasSuffix(pluginName, "Plugin") {
log.Warnf("Skipping plugin %q: name must end with 'Plugin'", pluginName)
continue
}

// Use exact plugin metadata name in stage directory
stageName := internalTransform.GenerateStageName(priority, pluginName)

log.Infof("Creating default stage for plugin: %s -> %s", pluginName, stageName)

if err := createStageDirectory(transformDir, stageName, orchestrator); err != nil {
return nil, err
}

stageNames = append(stageNames, stageName)
priority += 5

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

Use the same priority step here as the other stage-creation paths.

This helper advances priorities by 5, but GenerateStageDirNames() and ad-hoc stage creation both use 10-step spacing. That makes a repo bootstrapped with plain transform produce 10_Foo, 15_Bar, while the equivalent --instructions-file run expects 10_Foo, 20_Bar; reconcileInstructionStages() then treats the default-created stage dirs as extras and errors or deletes them.

Suggested fix
 	// Assign priority to each plugin
-	// Start at 10, increment by 5 for each plugin
+	// Start at 10, increment by 10 to stay consistent with
+	// instructions-file and ad-hoc stage generation.
 	priority := 10
@@
 		stageNames = append(stageNames, stageName)
-		priority += 5
+		priority += 10
 	}
📝 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
// Sort plugins by name for deterministic ordering
sort.Slice(allPlugins, func(i, j int) bool {
return allPlugins[i].Metadata().Name < allPlugins[j].Metadata().Name
})
// Assign priority to each plugin
// Start at 10, increment by 5 for each plugin
priority := 10
for _, plugin := range allPlugins {
pluginName := plugin.Metadata().Name
// Validate plugin name is safe to use as directory name
if err := validateStageNameToken(pluginName); err != nil {
log.Warnf("Skipping plugin %q: %v", pluginName, err)
continue
}
// Require "Plugin" suffix
if !strings.HasSuffix(pluginName, "Plugin") {
log.Warnf("Skipping plugin %q: name must end with 'Plugin'", pluginName)
continue
}
// Use exact plugin metadata name in stage directory
stageName := internalTransform.GenerateStageName(priority, pluginName)
log.Infof("Creating default stage for plugin: %s -> %s", pluginName, stageName)
if err := createStageDirectory(transformDir, stageName, orchestrator); err != nil {
return nil, err
}
stageNames = append(stageNames, stageName)
priority += 5
// Sort plugins by name for deterministic ordering
sort.Slice(allPlugins, func(i, j int) bool {
return allPlugins[i].Metadata().Name < allPlugins[j].Metadata().Name
})
// Assign priority to each plugin
// Start at 10, increment by 10 to stay consistent with
// instructions-file and ad-hoc stage generation.
priority := 10
for _, plugin := range allPlugins {
pluginName := plugin.Metadata().Name
// Validate plugin name is safe to use as directory name
if err := validateStageNameToken(pluginName); err != nil {
log.Warnf("Skipping plugin %q: %v", pluginName, err)
continue
}
// Require "Plugin" suffix
if !strings.HasSuffix(pluginName, "Plugin") {
log.Warnf("Skipping plugin %q: name must end with 'Plugin'", pluginName)
continue
}
// Use exact plugin metadata name in stage directory
stageName := internalTransform.GenerateStageName(priority, pluginName)
log.Infof("Creating default stage for plugin: %s -> %s", pluginName, stageName)
if err := createStageDirectory(transformDir, stageName, orchestrator); err != nil {
return nil, err
}
stageNames = append(stageNames, stageName)
priority += 10
}
🤖 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/transform/transform.go` around lines 449 - 483, The default plugin stage
priority spacing is inconsistent: in the loop that creates default stages (using
GenerateStageName, createStageDirectory, and the local variable priority) it
currently increments priority by 5 which conflicts with GenerateStageDirNames()
and ad-hoc creation that use 10-step spacing; update the loop to advance
priority by 10 (leave the initial priority = 10) and adjust the comment to
reflect 10-step increments so generated stage names match other paths and
reconcileInstructionStages() won’t treat them as extras.

Comment on lines +225 to +276
By("Skipped - Verify PVC data was transferred intact by running a reader pod on target")
// TODO(https://github.com/migtools/crane/issues/408): PVC data integrity assertion
// is commented out due to flaky behavior on Linux (locally and on CI). On macOS,
// migration works correctly as namespace-admin and PVC content is transferred
// consistently. On Linux however, even with cluster-admin contexts, the PVC transfers
// (Bound on target) but the content is sometimes empty with no error surfaced.
//
// When investigating the crane source, the rsync server health check result was found
// to be silently discarded in cmd/transfer-pvc/transfer-pvc.go — if the server never
// becomes healthy, the command proceeds anyway with no error, making it harder to
// diagnose why the content isn't being transferred on Linux.
//
// Re-enable this assertion once crane#408 is resolved.
// _, err = scenario.KubectlTgtNonAdmin.Run(
// "run", "pvc-reader",
// "-n", namespace,
// "--image=busybox",
// "--restart=Never",
// fmt.Sprintf(`--overrides={
// "spec": {
// "containers": [{
// "name": "pvc-reader",
// "image": "busybox",
// "command": ["sh", "-c", "cat /data/log.txt || echo FILE_NOT_FOUND"],
// "volumeMounts": [{"name":"data","mountPath":"/data"}]
// }],
// "volumes": [{"name":"data","persistentVolumeClaim":{"claimName":"%s"}}],
// "restartPolicy": "Never"
// }
// }`, pvcName),
// )
// Expect(err).NotTo(HaveOccurred())

Eventually(func() string {
out, err := scenario.KubectlTgtNonAdmin.Run(
"get", "pod", "pvc-reader",
"-n", namespace,
"-o", "jsonpath={.status.phase}",
)
if err != nil {
return ""
}
return out
}, "2m", "5s").Should(Or(Equal("Succeeded"), Equal("Failed")))
// Eventually(func() string {
// out, err := scenario.KubectlTgtNonAdmin.Run(
// "get", "pod", "pvc-reader",
// "-n", namespace,
// "-o", "jsonpath={.status.phase}",
// )
// if err != nil {
// return ""
// }
// return out
// }, "2m", "5s").Should(Or(Equal("Succeeded"), Equal("Failed")))

pvcReaderLogs, err := scenario.KubectlTgtNonAdmin.Run("logs", "pvc-reader", "-n", namespace)
Expect(err).NotTo(HaveOccurred())
Expect(pvcReaderLogs).To(ContainSubstring(expectedLogSubstring),
"expected PVC log.txt to contain data written on source cluster")
log.Printf("PVC data integrity confirmed, source log entries present on target\n")
// pvcReaderLogs, err := scenario.KubectlTgtNonAdmin.Run("logs", "pvc-reader", "-n", namespace)
// Expect(err).NotTo(HaveOccurred())
// Expect(pvcReaderLogs).To(ContainSubstring(expectedLogSubstring),
// "expected PVC log.txt to contain data written on source cluster")
// log.Printf("PVC data integrity confirmed, source log entries present on target\n")

_, _ = scenario.KubectlTgtNonAdmin.Run("delete", "pod", "pvc-reader", "-n", namespace, "--ignore-not-found=true")
// _, _ = scenario.KubectlTgtNonAdmin.Run("delete", "pod", "pvc-reader", "-n", namespace, "--ignore-not-found=true")

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

Don’t let this spec pass without proving PVC contents were migrated.

With this block disabled, the test only proves the target CronJob can start and write fresh data after unsuspending. It no longer verifies that transfer-pvc preserved the source data, so this spec can go green while the BUG #408 behavior still exists. Either mark the whole spec pending/skip until the integrity check is stable, or keep a non-flaky assertion against the transferred PVC contents so the test name and bug labels stay accurate.

Based on learnings: "Bug fixes should include regression tests 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 `@e2e-tests/tests/mta_813_cronJob_PVC_test.go` around lines 225 - 276, The test
currently disables the PVC data-integrity assertion so it no longer verifies
transfer-pvc preserved source data; either mark the whole spec pending/skip or
restore a non-flaky check: re-enable the pvc-reader flow (creating pod
"pvc-reader" via scenario.KubectlTgtNonAdmin.Run using pvcName/namespace), but
make the assertion robust by waiting for the pod to reach a terminal phase with
a longer Eventually timeout and retrying logs until they contain
expectedLogSubstring (use Eventually around
scenario.KubectlTgtNonAdmin.Run("logs", "pvc-reader", "-n", namespace) and
assert it Contains expectedLogSubstring), or if flakiness persists, add a Ginkgo
Skip at the top of the spec referencing crane#408 until that bug is fixed.

Comment on lines +312 to +331
func TestCaptureAPISurfaceScriptPath(t *testing.T) {
// Ensure helper returns an absolute, existing path to scripts/capture-api-surface.sh.
scriptPath, err := CaptureAPISurfaceScriptPath()
if err != nil {
t.Fatalf("CaptureAPISurfaceScriptPath: %v", err)
}

if !filepath.IsAbs(scriptPath) {
t.Fatalf("script path must be absolute, got %q", scriptPath)
}

wantSuffix := filepath.Join("scripts", "capture-api-surface.sh")
if !strings.HasSuffix(filepath.Clean(scriptPath), wantSuffix) {
t.Fatalf("script path %q must end with %q", scriptPath, wantSuffix)
}

if _, err := os.Stat(scriptPath); err != nil {
t.Fatalf("os.Stat(%q): %v", scriptPath, err)
}
}

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 | 🟡 Minor | ⚡ Quick win

Add direct regression coverage for TestdataFilePath() as well.

utils.go added two path helpers, but this file only tests one of them. A small table-driven test for the happy path plus the empty-filename error would keep the new instructions-file path contract covered without relying only on the E2E suite.

Suggested test shape
+func TestTestdataFilePath(t *testing.T) {
+	tests := []struct {
+		name        string
+		filename    string
+		wantErr     bool
+		wantSuffix  string
+	}{
+		{
+			name:       "existing_file",
+			filename:   "basic-instructions-file.yaml",
+			wantSuffix: filepath.Join("e2e-tests", "testdata", "basic-instructions-file.yaml"),
+		},
+		{
+			name:     "empty_filename",
+			filename: "",
+			wantErr:  true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := TestdataFilePath(tt.filename)
+			if tt.wantErr {
+				if err == nil {
+					t.Fatal("expected error, got nil")
+				}
+				return
+			}
+			if err != nil {
+				t.Fatalf("TestdataFilePath(%q): %v", tt.filename, err)
+			}
+			if !filepath.IsAbs(got) {
+				t.Fatalf("path must be absolute, got %q", got)
+			}
+			if !strings.HasSuffix(filepath.Clean(got), tt.wantSuffix) {
+				t.Fatalf("path %q must end with %q", got, tt.wantSuffix)
+			}
+		})
+	}
+}

As per coding guidelines, **/*_test.go: All new features require tests.

📝 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 TestCaptureAPISurfaceScriptPath(t *testing.T) {
// Ensure helper returns an absolute, existing path to scripts/capture-api-surface.sh.
scriptPath, err := CaptureAPISurfaceScriptPath()
if err != nil {
t.Fatalf("CaptureAPISurfaceScriptPath: %v", err)
}
if !filepath.IsAbs(scriptPath) {
t.Fatalf("script path must be absolute, got %q", scriptPath)
}
wantSuffix := filepath.Join("scripts", "capture-api-surface.sh")
if !strings.HasSuffix(filepath.Clean(scriptPath), wantSuffix) {
t.Fatalf("script path %q must end with %q", scriptPath, wantSuffix)
}
if _, err := os.Stat(scriptPath); err != nil {
t.Fatalf("os.Stat(%q): %v", scriptPath, err)
}
}
func TestCaptureAPISurfaceScriptPath(t *testing.T) {
// Ensure helper returns an absolute, existing path to scripts/capture-api-surface.sh.
scriptPath, err := CaptureAPISurfaceScriptPath()
if err != nil {
t.Fatalf("CaptureAPISurfaceScriptPath: %v", err)
}
if !filepath.IsAbs(scriptPath) {
t.Fatalf("script path must be absolute, got %q", scriptPath)
}
wantSuffix := filepath.Join("scripts", "capture-api-surface.sh")
if !strings.HasSuffix(filepath.Clean(scriptPath), wantSuffix) {
t.Fatalf("script path %q must end with %q", scriptPath, wantSuffix)
}
if _, err := os.Stat(scriptPath); err != nil {
t.Fatalf("os.Stat(%q): %v", scriptPath, err)
}
}
func TestTestdataFilePath(t *testing.T) {
tests := []struct {
name string
filename string
wantErr bool
wantSuffix string
}{
{
name: "existing_file",
filename: "basic-instructions-file.yaml",
wantSuffix: filepath.Join("e2e-tests", "testdata", "basic-instructions-file.yaml"),
},
{
name: "empty_filename",
filename: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := TestdataFilePath(tt.filename)
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("TestdataFilePath(%q): %v", tt.filename, err)
}
if !filepath.IsAbs(got) {
t.Fatalf("path must be absolute, got %q", got)
}
if !strings.HasSuffix(filepath.Clean(got), tt.wantSuffix) {
t.Fatalf("path %q must end with %q", got, tt.wantSuffix)
}
})
}
}
🤖 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 `@e2e-tests/utils/utils_test.go` around lines 312 - 331, Add direct unit tests
for the TestdataFilePath helper: create a new table-driven test in
e2e-tests/utils/utils_test.go (alongside TestCaptureAPISurfaceScriptPath) that
exercises TestdataFilePath() for the happy path (valid filename returns an
absolute, existing path ending with the expected suffix) and the error path
(empty filename returns the expected error). Reference the TestdataFilePath
function name to locate the helper, assert filepath.IsAbs, strings.HasSuffix on
filepath.Clean(result), and that os.Stat succeeds for the valid case, and assert
a non-nil/expected error for the empty-filename case.

Comment thread e2e-tests/utils/utils.go
Comment on lines +104 to +115
func TestdataFilePath(filename string) (string, error) {
if filename == "" {
return "", fmt.Errorf("filename is required")
}
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
return "", fmt.Errorf("runtime.Caller failed")
}
baseDir := filepath.Dir(thisFile)
path := filepath.Join(baseDir, "..", "testdata", filename)
return path, 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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Go, does runtime.Caller guarantee that the returned filename is absolute? How can -trimpath or different build modes affect runtime.Caller path values?

💡 Result:

In Go, the runtime.Caller function does not guarantee that the returned filename is an absolute path [1][2]. By default, without specific build flags, it typically returns the absolute path to the source file as it existed on the machine where the binary was compiled [3][4][5]. Because these paths are embedded into the binary at compile time, they reflect the build environment rather than the environment where the binary is currently executing [3][4]. The -trimpath build flag significantly alters this behavior to support reproducible builds [6][7]. When -trimpath is used: 1. Local filesystem paths are removed from the recorded file information in the resulting executable [6][8]. 2. Instead of absolute paths, the file names begin with a path independent of the local filesystem [7][8]. - For standard library packages, paths typically start with "go" [7][8]. - For module-based projects, paths typically start with the module path (and version, if applicable) [6][7]. - For other packages, they may appear as a plain import path [7][8]. Because these changes affect the string returned by runtime.Caller, any code that relies on these paths (e.g., to derive directory structures or locate project roots) may break or behave unexpectedly when -trimpath is enabled [4][9]. Additionally, the returned filename consistently uses forward slashes (/) as path separators, regardless of the host operating system, including Windows [1][6]. Other build modes or factors like the use of //line directives can also influence the specific path string returned, as they allow manual overriding of source position information [5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the current implementations around the referenced line ranges
echo "=== utils.go: TestdataFilePath (around 104-115) ==="
sed -n '80,140p' e2e-tests/utils/utils.go

echo
echo "=== utils.go: CaptureAPISurfaceScriptPath (around 880-930) ==="
sed -n '860,930p' e2e-tests/utils/utils.go

echo
echo "=== Search for TestdataFilePath usage ==="
rg -n "TestdataFilePath\\(" -S e2e-tests || true

echo
echo "=== Search for CaptureAPISurfaceScriptPath usage ==="
rg -n "CaptureAPISurfaceScriptPath\\(" -S e2e-tests || true

echo
echo "=== Inspect the MTA-828 instructions file migration test ==="
rg -n "mta_828|instructions-file|basic-instructions-file\\.yaml|WorkDir" -S e2e-tests/tests || true

Repository: migtools/mta-crane

Length of output: 7941


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Inspect utils_test.go around CaptureAPISurfaceScriptPath test ==="
sed -n '260,380p' e2e-tests/utils/utils_test.go

echo
echo "=== Inspect any tests for TestdataFilePath ==="
rg -n "TestdataFilePath\\(" e2e-tests/utils -S || true
rg -n "TestdataFilePath" e2e-tests -S || true

echo
echo "=== Search repo for -trimpath usage ==="
rg -n --hidden --no-ignore-vcs "trimpath" -S . || true

Repository: migtools/mta-crane

Length of output: 4513


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search workflows for go test / trimpath ==="
rg -n "go test|-trimpath" .github/workflows -S || true

echo
echo "=== Inspect mta_828_instructions_file_migration_test.go ==="
sed -n '1,140p' e2e-tests/tests/mta_828_instructions_file_migration_test.go

echo
echo "=== Inspect mta_830_instructions_file_force_reconcile_test.go ==="
sed -n '1,200p' e2e-tests/tests/mta_830_instructions_file_force_reconcile_test.go

Repository: migtools/mta-crane

Length of output: 11176


Make TestdataFilePath / CaptureAPISurfaceScriptPath return absolute filesystem paths

runtime.Caller(0) doesn’t guarantee an absolute filename (notably when builds use -trimpath), and both helpers currently do only filepath.Join, which means they can return relative/trimmed paths—breaking the stated contracts. This is especially important because CaptureAPISurfaceScriptPath is tested to always return an absolute path (filepath.IsAbs in TestCaptureAPISurfaceScriptPath), and TestdataFilePath is used by MTA-828/MTA-830 after runner.WorkDir is set to a temp directory, so a relative result would resolve against the wrong working dir.

Suggested fix
 func TestdataFilePath(filename string) (string, error) {
 	if filename == "" {
 		return "", fmt.Errorf("filename is required")
 	}
 	_, thisFile, _, ok := runtime.Caller(0)
 	if !ok {
 		return "", fmt.Errorf("runtime.Caller failed")
 	}
 	baseDir := filepath.Dir(thisFile)
-	path := filepath.Join(baseDir, "..", "testdata", filename)
-	return path, nil
+	path, err := filepath.Abs(filepath.Join(baseDir, "..", "testdata", filename))
+	if err != nil {
+		return "", fmt.Errorf("resolve testdata path for %q: %w", filename, err)
+	}
+	return path, nil
 }
@@
 func CaptureAPISurfaceScriptPath() (string, error) {
 	_, thisFile, _, ok := runtime.Caller(0)
 	if !ok {
 		return "", fmt.Errorf("runtime.Caller failed")
 	}
@@
 	baseDir := filepath.Dir(thisFile)
-	scriptPath := filepath.Join(baseDir, "..", "..", "scripts", "capture-api-surface.sh")
+	scriptPath, err := filepath.Abs(filepath.Join(baseDir, "..", "..", "scripts", "capture-api-surface.sh"))
+	if err != nil {
+		return "", fmt.Errorf("resolve capture script path: %w", err)
+	}
 
 	if _, err := os.Stat(scriptPath); err != nil {
 		return "", fmt.Errorf("capture script not found at %s: %w", scriptPath, err)
 	}
 
 	return scriptPath, 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 TestdataFilePath(filename string) (string, error) {
if filename == "" {
return "", fmt.Errorf("filename is required")
}
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
return "", fmt.Errorf("runtime.Caller failed")
}
baseDir := filepath.Dir(thisFile)
path := filepath.Join(baseDir, "..", "testdata", filename)
return path, nil
}
func TestdataFilePath(filename string) (string, error) {
if filename == "" {
return "", fmt.Errorf("filename is required")
}
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
return "", fmt.Errorf("runtime.Caller failed")
}
baseDir := filepath.Dir(thisFile)
path, err := filepath.Abs(filepath.Join(baseDir, "..", "testdata", filename))
if err != nil {
return "", fmt.Errorf("resolve testdata path for %q: %w", filename, err)
}
return path, 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 `@e2e-tests/utils/utils.go` around lines 104 - 115, runtime.Caller(0) can
return a trimmed/relative filename, so update TestdataFilePath (and likewise
CaptureAPISurfaceScriptPath) to resolve absolute filesystem paths: after
obtaining thisFile from runtime.Caller(0) call filepath.Abs(thisFile) (handle
the error), use filepath.Dir on that absolute path, build the final path with
filepath.Join, then call filepath.Abs on the final path (or return the already
absolute result) and propagate any errors; ensure both functions return an
absolute path and proper error on failure.

Comment on lines +92 to +108
for _, requested := range selector.Stages {
for _, stage := range allStages {
// Skip if already added
if seen[stage.DirName] {
continue
}

// Match by directory name OR plugin name
if stage.DirName == requested || stage.PluginName == requested {
filtered = append(filtered, stage)
seen[stage.DirName] = true
break
}
}
}

// Stage not found, return empty
return []Stage{}
return filtered

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

Preserve pipeline order when selecting multiple stages.

FilterStages now returns matches in the caller's argument order, but RunMultiStage executes selectedStages sequentially and feeds each later stage from the previous selected stage's output. A selector like ["30_ImagestreamPlugin", "10_KubernetesPlugin"] will therefore run stage 10 against stage 30 output, which is the wrong dependency chain.

Suggested fix
 func FilterStages(allStages []Stage, selector StageSelector) []Stage {
 	// If no stages specified, return all stages
 	if len(selector.Stages) == 0 {
 		return allStages
 	}
 
-	var filtered []Stage
-	seen := make(map[string]bool) // Prevent duplicates
-
-	for _, requested := range selector.Stages {
-		for _, stage := range allStages {
-			// Skip if already added
-			if seen[stage.DirName] {
-				continue
-			}
-
-			// Match by directory name OR plugin name
-			if stage.DirName == requested || stage.PluginName == requested {
-				filtered = append(filtered, stage)
-				seen[stage.DirName] = true
-				break
-			}
-		}
-	}
+	requested := make(map[string]struct{}, len(selector.Stages))
+	for _, name := range selector.Stages {
+		requested[name] = struct{}{}
+	}
+
+	filtered := make([]Stage, 0, len(selector.Stages))
+	seen := make(map[string]bool, len(selector.Stages))
+	for _, stage := range allStages {
+		if seen[stage.DirName] {
+			continue
+		}
+		if _, ok := requested[stage.DirName]; ok {
+			filtered = append(filtered, stage)
+			seen[stage.DirName] = true
+			continue
+		}
+		if _, ok := requested[stage.PluginName]; ok {
+			filtered = append(filtered, stage)
+			seen[stage.DirName] = true
+		}
+	}
 
 	return filtered
 }
📝 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
for _, requested := range selector.Stages {
for _, stage := range allStages {
// Skip if already added
if seen[stage.DirName] {
continue
}
// Match by directory name OR plugin name
if stage.DirName == requested || stage.PluginName == requested {
filtered = append(filtered, stage)
seen[stage.DirName] = true
break
}
}
}
// Stage not found, return empty
return []Stage{}
return filtered
for _, requested := range selector.Stages {
for _, stage := range allStages {
// Skip if already added
if seen[stage.DirName] {
continue
}
// Match by directory name OR plugin name
if stage.DirName == requested || stage.PluginName == requested {
filtered = append(filtered, stage)
seen[stage.DirName] = true
break
}
}
}
requested := make(map[string]struct{}, len(selector.Stages))
for _, name := range selector.Stages {
requested[name] = struct{}{}
}
filtered := make([]Stage, 0, len(selector.Stages))
seen := make(map[string]bool, len(selector.Stages))
for _, stage := range allStages {
if seen[stage.DirName] {
continue
}
if _, ok := requested[stage.DirName]; ok {
filtered = append(filtered, stage)
seen[stage.DirName] = true
continue
}
if _, ok := requested[stage.PluginName]; ok {
filtered = append(filtered, stage)
seen[stage.DirName] = true
}
}
return filtered
🤖 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/transform/stages.go` around lines 92 - 108, FilterStages currently
returns matches in selector.Stages order which breaks RunMultiStage's dependency
chain; change FilterStages to iterate over allStages and append a stage only if
its DirName or PluginName is present in selector.Stages (use a lookup set built
from selector.Stages) while still skipping duplicates via seen, so the returned
filtered slice preserves the pipeline order as defined by allStages; update
references inside FilterStages to use selector.Stages lookup, seen, filtered,
allStages, stage.DirName and stage.PluginName accordingly.

Comment on lines +545 to +547
if !hasKustomizeCommand(t) {
t.Skip("kubectl/oc not available, skipping kustomize build test")
}

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

Remove the kubectl/oc gate from this embedded-runner test.

applyStageTransforms() now exercises the in-process kustomize runner, so this skip condition makes the new regression test disappear in environments that lack kubectl/oc even though the code under test no longer shells out to either binary. As per coding guidelines, "All new features require tests".

Suggested fix
 func TestWriteStage_KustomizeBuildWithMixedResources(t *testing.T) {
-	if !hasKustomizeCommand(t) {
-		t.Skip("kubectl/oc not available, skipping kustomize build test")
-	}
-
 	tmpDir, err := os.MkdirTemp("", "crane-writer-kustomize-mixed-*")
 	if err != nil {
 		t.Fatalf("Failed to create temp dir: %v", err)
@@
-	// Run kubectl kustomize to verify the output is valid
+	// Run embedded kustomize to verify the output is valid
 	stageDir := filepath.Join(transformDir, stageName)
@@
 	resources, err := o.applyStageTransforms(stageDir)
 	if err != nil {
-		t.Fatalf("kubectl kustomize failed on mixed resources: %v", err)
+		t.Fatalf("embedded kustomize build failed on mixed resources: %v", err)
 	}

Also applies to: 630-639

🤖 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/transform/writer_integration_test.go` around lines 545 - 547, The
test currently skips when kubectl/oc are missing via hasKustomizeCommand(t), but
applyStageTransforms() now uses the in-process kustomize runner so remove that
external-gate: delete the conditional skip block that calls
hasKustomizeCommand(t) in writer_integration_test.go and any other identical
checks around the 630-639 region so the embedded-runner regression test always
runs; keep the rest of the test intact (it exercises applyStageTransforms()) and
ensure no remaining references to hasKustomizeCommand affect this test.

@aufi aufi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM, thank you!

@msajidmansoori12 msajidmansoori12 merged commit 9475110 into main Jun 4, 2026
5 of 6 checks passed
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.

5 participants