Skip to content

Conversation

@pedjak
Copy link
Contributor

@pedjak pedjak commented Nov 13, 2025

Description

Replaces the monolithic 170-line reconcile() method with a flexible step-based architecture that executes discrete reconciliation phases in sequence. Each phase (HandleFinalizers, RetrieveRevisionStates, RetrieveRevisionMetadata, UnpackBundle, ApplyBundle) is now a standalone function that can be composed differently for Helm vs Boxcutter workflows.

Changes:

  • Introduce ReconcileStepFunc type and ReconcileSteps executor
  • Extract reconcile logic into individual step functions in new file clusterextension_reconcile_steps.go
  • Move BoxcutterRevisionStatesGetter to boxcutter_reconcile_steps.go alongside MigrateStorage step
  • Configure step pipelines in main.go for each applier type
  • Refactor tests to use functional options pattern for reconciler setup

Reviewer Checklist

  • API Go Documentation
  • Tests: Unit Tests (and E2E Tests, if appropriate)
  • Comprehensive Commit Messages
  • Links to related GitHub Issue(s)

…ipeline

Replaces the monolithic 170-line reconcile() method with a flexible
step-based architecture that executes discrete reconciliation phases in
sequence. Each phase (`HandleFinalizers`, `RetrieveRevisionStates`,
`RetrieveRevisionMetadata`, `UnpackBundle`, `ApplyBundle`) is now a standalone
function that can be composed differently for Helm vs Boxcutter workflows.

Changes:
  - Introduce `ReconcileStepFunc` type and `ReconcileSteps` executor
  - Extract reconcile logic into individual step functions in new file
    `clusterextension_reconcile_steps.go`
  - Move `BoxcutterRevisionStatesGetter` to `boxcutter_reconcile_steps.go`
    alongside `MigrateStorage` step
  - Configure step pipelines in `main.go` for each applier type
  - Refactor tests to use functional options pattern for reconciler setup
Copilot AI review requested due to automatic review settings November 13, 2025 15:54
@pedjak pedjak requested a review from a team as a code owner November 13, 2025 15:54
@netlify
Copy link

netlify bot commented Nov 13, 2025

Deploy Preview for olmv1 ready!

Name Link
🔨 Latest commit e9ab9dc
🔍 Latest deploy log https://app.netlify.com/projects/olmv1/deploys/6915ff3a445df700083110ba
😎 Deploy Preview https://deploy-preview-2332--olmv1.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@openshift-ci
Copy link

openshift-ci bot commented Nov 13, 2025

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign grokspawn, thetechnick for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@pedjak pedjak changed the title Refactor ClusterExtension reconciler to use composable step-based pipeline 🌱 Refactor ClusterExtension reconciler to use composable step-based pipeline Nov 13, 2025
Copilot finished reviewing on behalf of pedjak November 13, 2025 15:59
@pedjak pedjak requested review from perdasilva and removed request for bentito and thetechnick November 13, 2025 16:00
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR refactors the ClusterExtension reconciler from a monolithic 170-line reconcile method into a composable step-based pipeline architecture. Each reconciliation phase (finalizer handling, revision state retrieval, metadata resolution, bundle unpacking, and bundle application) is now a standalone function that can be configured differently for Helm vs Boxcutter workflows.

  • Introduces ReconcileStepFunc type and ReconcileSteps executor for sequential step execution
  • Extracts reconciliation logic into individual step functions with context-based data passing
  • Moves Boxcutter-specific code (BoxcutterRevisionStatesGetter, MigrateStorage) into dedicated file
  • Configures applier-specific pipelines in main.go
  • Updates tests to use functional options pattern for reconciler setup

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
internal/operator-controller/controllers/clusterextension_controller.go Removes monolithic reconcile method, adds ReconcileStepFunc infrastructure and step executor
internal/operator-controller/controllers/clusterextension_reconcile_steps.go New file containing common reconciliation step functions with context-based data flow
internal/operator-controller/controllers/boxcutter_reconcile_steps.go New file containing boxcutter-specific steps (MigrateStorage) and BoxcutterRevisionStatesGetter
cmd/operator-controller/main.go Configures reconciliation step pipelines for both boxcutter and Helm appliers
internal/operator-controller/controllers/suite_test.go Adds functional options pattern for test reconciler configuration
internal/operator-controller/controllers/clusterextension_controller_test.go Refactors tests to use new functional options pattern for reconciler setup

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


for _, applyOpt := range opts {
applyOpt(ctrlBuilder)
}
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

[nitpick] The removal of the blank line at line 312 reduces code readability. There should be a blank line between the loop and the return statement to separate the loop logic from the function return.

Consider keeping the blank line for better visual separation.

Suggested change
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +156
func HandleFinalizers(f finalizer.Finalizer) ReconcileStepFunc {
return func(ctx context.Context, ext *ocv1.ClusterExtension) (context.Context, *ctrl.Result, error) {
l := log.FromContext(ctx)

l.Info("handling finalizers")
finalizeResult, err := f.Finalize(ctx, ext)
if err != nil {
setStatusProgressing(ext, err)
return ctx, nil, err
}
if finalizeResult.Updated || finalizeResult.StatusUpdated {
// On create: make sure the finalizer is applied before we do anything
// On delete: make sure we do nothing after the finalizer is removed
return ctx, &ctrl.Result{}, nil
}

if ext.GetDeletionTimestamp() != nil {
// If we've gotten here, that means the cluster extension is being deleted, we've handled all of
// _our_ finalizers (above), but the cluster extension is still present in the cluster, likely
// because there are _other_ finalizers that other controllers need to handle, (e.g. the orphan
// deletion finalizer).
return nil, nil, nil
}
return ctx, nil, nil
}
}

func RetrieveRevisionStates(r RevisionStatesGetter) ReconcileStepFunc {
return func(ctx context.Context, ext *ocv1.ClusterExtension) (context.Context, *ctrl.Result, error) {
l := log.FromContext(ctx)
l.Info("getting installed bundle")
revisionStates, err := r.GetRevisionStates(ctx, ext)
if err != nil {
setInstallStatus(ext, nil)
var saerr *authentication.ServiceAccountNotFoundError
if errors.As(err, &saerr) {
setInstalledStatusConditionUnknown(ext, saerr.Error())
setStatusProgressing(ext, errors.New("installation cannot proceed due to missing ServiceAccount"))
return ctx, nil, err
}
setInstalledStatusConditionUnknown(ext, err.Error())
setStatusProgressing(ext, errors.New("retrying to get installed bundle"))
return ctx, nil, err
}
return context.WithValue(ctx, revisionStatesKey{}, revisionStates), nil, nil
}
}

func RetrieveRevisionMetadata(r resolve.Resolver) ReconcileStepFunc {
return func(ctx context.Context, ext *ocv1.ClusterExtension) (context.Context, *ctrl.Result, error) {
l := log.FromContext(ctx)
revisionStates := ctx.Value(revisionStatesKey{}).(*RevisionStates)
var resolvedRevisionMetadata *RevisionMetadata
if len(revisionStates.RollingOut) == 0 {
l.Info("resolving bundle")
var bm *ocv1.BundleMetadata
if revisionStates.Installed != nil {
bm = &revisionStates.Installed.BundleMetadata
}
resolvedBundle, resolvedBundleVersion, resolvedDeprecation, err := r.Resolve(ctx, ext, bm)
if err != nil {
// Note: We don't distinguish between resolution-specific errors and generic errors
setStatusProgressing(ext, err)
setInstalledStatusFromRevisionStates(ext, revisionStates)
ensureAllConditionsWithReason(ext, ocv1.ReasonFailed, err.Error())
return ctx, nil, err
}

// set deprecation status after _successful_ resolution
// TODO:
// 1. It seems like deprecation status should reflect the currently installed bundle, not the resolved
// bundle. So perhaps we should set package and channel deprecations directly after resolution, but
// defer setting the bundle deprecation until we successfully install the bundle.
// 2. If resolution fails because it can't find a bundle, that doesn't mean we wouldn't be able to find
// a deprecation for the ClusterExtension's spec.packageName. Perhaps we should check for a non-nil
// resolvedDeprecation even if resolution returns an error. If present, we can still update some of
// our deprecation status.
// - Open question though: what if different catalogs have different opinions of what's deprecated.
// If we can't resolve a bundle, how do we know which catalog to trust for deprecation information?
// Perhaps if the package shows up in multiple catalogs and deprecations don't match, we can set
// the deprecation status to unknown? Or perhaps we somehow combine the deprecation information from
// all catalogs?
SetDeprecationStatus(ext, resolvedBundle.Name, resolvedDeprecation)
resolvedRevisionMetadata = &RevisionMetadata{
Package: resolvedBundle.Package,
Image: resolvedBundle.Image,
BundleMetadata: bundleutil.MetadataFor(resolvedBundle.Name, *resolvedBundleVersion),
}
} else {
resolvedRevisionMetadata = revisionStates.RollingOut[0]
}
return context.WithValue(ctx, resolvedRevisionMetadataKey{}, resolvedRevisionMetadata), nil, nil
}
}

func UnpackBundle(i imageutil.Puller, cache imageutil.Cache) ReconcileStepFunc {
return func(ctx context.Context, ext *ocv1.ClusterExtension) (context.Context, *ctrl.Result, error) {
l := log.FromContext(ctx)
l.Info("unpacking resolved bundle")
resolvedRevisionMetadata := ctx.Value(resolvedRevisionMetadataKey{}).(*RevisionMetadata)
imageFS, _, _, err := i.Pull(ctx, ext.GetName(), resolvedRevisionMetadata.Image, cache)
if err != nil {
revisionStates := ctx.Value(revisionStatesKey{}).(*RevisionStates)
// Wrap the error passed to this with the resolution information until we have successfully
// installed since we intend for the progressing condition to replace the resolved condition
// and will be removing the .status.resolution field from the ClusterExtension status API
setStatusProgressing(ext, wrapErrorWithResolutionInfo(resolvedRevisionMetadata.BundleMetadata, err))
setInstalledStatusFromRevisionStates(ext, revisionStates)
return ctx, nil, err
}
return context.WithValue(ctx, imageFSKey{}, imageFS), nil, nil
}
}

func ApplyBundle(a Applier) ReconcileStepFunc {
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

The exported functions HandleFinalizers, RetrieveRevisionStates, RetrieveRevisionMetadata, UnpackBundle, and ApplyBundle lack documentation. These are the core building blocks of the new reconciliation architecture and should have doc comments explaining their purpose, inputs, outputs, and any side effects.

Example for HandleFinalizers:

// HandleFinalizers returns a ReconcileStepFunc that manages finalizers for the ClusterExtension.
// It ensures finalizers are added on resource creation and properly handles cleanup on deletion.
// If finalizers are updated, reconciliation stops to allow the changes to be persisted.
func HandleFinalizers(f finalizer.Finalizer) ReconcileStepFunc {

Copilot uses AI. Check for mistakes.
return func(ctx context.Context, ext *ocv1.ClusterExtension) (context.Context, *ctrl.Result, error) {
l := log.FromContext(ctx)
l.Info("unpacking resolved bundle")
resolvedRevisionMetadata := ctx.Value(resolvedRevisionMetadataKey{}).(*RevisionMetadata)
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

The UnpackBundle step performs an unsafe type assertion on line 141: ctx.Value(resolvedRevisionMetadataKey{}).(*RevisionMetadata). If the previous RetrieveRevisionMetadata step is skipped or the context doesn't contain this key, this will panic.

Consider adding a nil check before the type assertion.

Suggested change
resolvedRevisionMetadata := ctx.Value(resolvedRevisionMetadataKey{}).(*RevisionMetadata)
val := ctx.Value(resolvedRevisionMetadataKey{})
resolvedRevisionMetadata, ok := val.(*RevisionMetadata)
if !ok || resolvedRevisionMetadata == nil {
return ctx, nil, errors.New("resolved revision metadata not found in context")
}

Copilot uses AI. Check for mistakes.
// - A potentially modified context for the next step
// - An optional reconciliation result that if non-nil will stop reconciliation
// - Any error that occurred during reconciliation, which will be returned to the caller
type ReconcileStepFunc func(context.Context, *ocv1.ClusterExtension) (context.Context, *ctrl.Result, error)
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

The ReconcileSteps type (line 67) lacks documentation. Since this is a central type in the refactored architecture, it should have a doc comment explaining its purpose and usage.

Consider adding:

// ReconcileSteps is an ordered collection of reconciliation steps that are executed sequentially.
// Each step receives the output context from the previous step, allowing data to flow through the pipeline.
type ReconcileSteps []ReconcileStepFunc
Suggested change
type ReconcileStepFunc func(context.Context, *ocv1.ClusterExtension) (context.Context, *ctrl.Result, error)
type ReconcileStepFunc func(context.Context, *ocv1.ClusterExtension) (context.Context, *ctrl.Result, error)
// ReconcileSteps is an ordered collection of reconciliation steps that are executed sequentially.
// Each step receives the output context from the previous step, allowing data to flow through the pipeline.

Copilot uses AI. Check for mistakes.
func RetrieveRevisionMetadata(r resolve.Resolver) ReconcileStepFunc {
return func(ctx context.Context, ext *ocv1.ClusterExtension) (context.Context, *ctrl.Result, error) {
l := log.FromContext(ctx)
revisionStates := ctx.Value(revisionStatesKey{}).(*RevisionStates)
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

The RetrieveRevisionStates step performs an unsafe type assertion on line 93: ctx.Value(revisionStatesKey{}).(*RevisionStates). If the previous RetrieveRevisionStates step is skipped or the context doesn't contain this key, this will panic.

Consider adding a nil check:

revisionStatesValue := ctx.Value(revisionStatesKey{})
if revisionStatesValue == nil {
    return ctx, nil, errors.New("revision states not found in context")
}
revisionStates := revisionStatesValue.(*RevisionStates)
Suggested change
revisionStates := ctx.Value(revisionStatesKey{}).(*RevisionStates)
revisionStatesValue := ctx.Value(revisionStatesKey{})
if revisionStatesValue == nil {
return ctx, nil, errors.New("revision states not found in context")
}
revisionStates, ok := revisionStatesValue.(*RevisionStates)
if !ok {
return ctx, nil, errors.New("revision states in context has unexpected type")
}

Copilot uses AI. Check for mistakes.
Comment on lines +159 to +161
resolvedRevisionMetadata := ctx.Value(resolvedRevisionMetadataKey{}).(*RevisionMetadata)
imageFS := ctx.Value(imageFSKey{}).(fs.FS)
revisionStates := ctx.Value(revisionStatesKey{}).(*RevisionStates)
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

The ApplyBundle step performs multiple unsafe type assertions (lines 159, 160, 161). If any of the preceding steps are skipped or fail to populate the context, these will panic:

  • ctx.Value(resolvedRevisionMetadataKey{}).(*RevisionMetadata)
  • ctx.Value(imageFSKey{}).(fs.FS)
  • ctx.Value(revisionStatesKey{}).(*RevisionStates)

Consider adding nil checks before all type assertions to prevent panics.

Suggested change
resolvedRevisionMetadata := ctx.Value(resolvedRevisionMetadataKey{}).(*RevisionMetadata)
imageFS := ctx.Value(imageFSKey{}).(fs.FS)
revisionStates := ctx.Value(revisionStatesKey{}).(*RevisionStates)
resolvedRevisionMetadataVal := ctx.Value(resolvedRevisionMetadataKey{})
if resolvedRevisionMetadataVal == nil {
return ctx, nil, errors.New("missing resolvedRevisionMetadata in context")
}
resolvedRevisionMetadata, ok := resolvedRevisionMetadataVal.(*RevisionMetadata)
if !ok {
return ctx, nil, errors.New("invalid type for resolvedRevisionMetadata in context")
}
imageFSVal := ctx.Value(imageFSKey{})
if imageFSVal == nil {
return ctx, nil, errors.New("missing imageFS in context")
}
imageFS, ok := imageFSVal.(fs.FS)
if !ok {
return ctx, nil, errors.New("invalid type for imageFS in context")
}
revisionStatesVal := ctx.Value(revisionStatesKey{})
if revisionStatesVal == nil {
return ctx, nil, errors.New("missing revisionStates in context")
}
revisionStates, ok := revisionStatesVal.(*RevisionStates)
if !ok {
return ctx, nil, errors.New("invalid type for revisionStates in context")
}

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +39

type revisionStatesKey struct{}
type resolvedRevisionMetadataKey struct{}
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

Missing documentation for the context value key types revisionStatesKey, resolvedRevisionMetadataKey, and imageFSKey. These types are used as context keys but lack any explanation of their purpose or what values they hold.

Consider adding doc comments:

// revisionStatesKey is the context key for storing *RevisionStates
type revisionStatesKey struct{}

// resolvedRevisionMetadataKey is the context key for storing *RevisionMetadata
type resolvedRevisionMetadataKey struct{}

// imageFSKey is the context key for storing fs.FS
type imageFSKey struct{}
Suggested change
type revisionStatesKey struct{}
type resolvedRevisionMetadataKey struct{}
// revisionStatesKey is the context key for storing *RevisionStates.
type revisionStatesKey struct{}
// resolvedRevisionMetadataKey is the context key for storing *RevisionMetadata.
type resolvedRevisionMetadataKey struct{}
// imageFSKey is the context key for storing fs.FS.

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +149
revisionStates := ctx.Value(revisionStatesKey{}).(*RevisionStates)
// Wrap the error passed to this with the resolution information until we have successfully
// installed since we intend for the progressing condition to replace the resolved condition
// and will be removing the .status.resolution field from the ClusterExtension status API
setStatusProgressing(ext, wrapErrorWithResolutionInfo(resolvedRevisionMetadata.BundleMetadata, err))
setInstalledStatusFromRevisionStates(ext, revisionStates)
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

The UnpackBundle step retrieves revisionStates from context on line 144 only to use it in error handling (lines 149), but it's never used if unpacking succeeds. This type assertion could panic if the context doesn't contain the key.

Consider either:

  1. Adding a nil check before the type assertion
  2. Moving this retrieval inside the error handling block where it's actually needed
Suggested change
revisionStates := ctx.Value(revisionStatesKey{}).(*RevisionStates)
// Wrap the error passed to this with the resolution information until we have successfully
// installed since we intend for the progressing condition to replace the resolved condition
// and will be removing the .status.resolution field from the ClusterExtension status API
setStatusProgressing(ext, wrapErrorWithResolutionInfo(resolvedRevisionMetadata.BundleMetadata, err))
setInstalledStatusFromRevisionStates(ext, revisionStates)
var revisionStates *RevisionStates
if v := ctx.Value(revisionStatesKey{}); v != nil {
if rs, ok := v.(*RevisionStates); ok {
revisionStates = rs
}
}
// Wrap the error passed to this with the resolution information until we have successfully
// installed since we intend for the progressing condition to replace the resolved condition
// and will be removing the .status.resolution field from the ClusterExtension status API
setStatusProgressing(ext, wrapErrorWithResolutionInfo(resolvedRevisionMetadata.BundleMetadata, err))
if revisionStates != nil {
setInstalledStatusFromRevisionStates(ext, revisionStates)
}

Copilot uses AI. Check for mistakes.
for _, opt := range opts {
opt(reconciler)
}
reconciler.ReconcileSteps = []controllers.ReconcileStepFunc{controllers.HandleFinalizers(reconciler.Finalizers), controllers.RetrieveRevisionStates(reconciler.RevisionStatesGetter)}
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

The newClientAndReconciler function unconditionally sets up ReconcileSteps starting with HandleFinalizers and RetrieveRevisionStates on line 94, but these steps depend on Finalizers and RevisionStatesGetter being non-nil respectively. If a test doesn't configure these via options, the steps will still be added but will fail at runtime.

Consider either:

  1. Checking if these dependencies are nil before adding their steps
  2. Ensuring all dependencies have sensible defaults

For example:

if reconciler.Finalizers != nil {
    reconciler.ReconcileSteps = append(reconciler.ReconcileSteps, controllers.HandleFinalizers(reconciler.Finalizers))
}
if reconciler.RevisionStatesGetter != nil {
    reconciler.ReconcileSteps = append(reconciler.ReconcileSteps, controllers.RetrieveRevisionStates(reconciler.RevisionStatesGetter))
}
Suggested change
reconciler.ReconcileSteps = []controllers.ReconcileStepFunc{controllers.HandleFinalizers(reconciler.Finalizers), controllers.RetrieveRevisionStates(reconciler.RevisionStatesGetter)}
reconciler.ReconcileSteps = []controllers.ReconcileStepFunc{}
if reconciler.Finalizers != nil {
reconciler.ReconcileSteps = append(reconciler.ReconcileSteps, controllers.HandleFinalizers(reconciler.Finalizers))
}
if reconciler.RevisionStatesGetter != nil {
reconciler.ReconcileSteps = append(reconciler.ReconcileSteps, controllers.RetrieveRevisionStates(reconciler.RevisionStatesGetter))
}

Copilot uses AI. Check for mistakes.
@codecov
Copy link

codecov bot commented Nov 13, 2025

Codecov Report

❌ Patch coverage is 97.53086% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.41%. Comparing base (c95fc24) to head (e9ab9dc).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...ontroller/controllers/boxcutter_reconcile_steps.go 88.57% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2332      +/-   ##
==========================================
+ Coverage   74.30%   74.41%   +0.11%     
==========================================
  Files          91       93       +2     
  Lines        7083     7137      +54     
==========================================
+ Hits         5263     5311      +48     
- Misses       1405     1408       +3     
- Partials      415      418       +3     
Flag Coverage Δ
e2e 45.88% <59.87%> (+0.23%) ⬆️
experimental-e2e 48.56% <83.33%> (+0.21%) ⬆️
unit 58.66% <62.34%> (+0.06%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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.

1 participant