From 312426b7d33f2d1f7cb9d20f54238364325c5afb Mon Sep 17 00:00:00 2001 From: Sanskar Jaiswal Date: Thu, 11 May 2023 01:47:27 +0530 Subject: [PATCH] git: add push.refspec to push using a refspec Add `.spec.git.push.refspec` to allow specifying a refspec to be used for performing a push operation. It takes precedence over the `spec.git.push.branch` field. Signed-off-by: Sanskar Jaiswal --- api/v1beta1/git.go | 11 +- ...lkit.fluxcd.io_imageupdateautomations.yaml | 8 +- docs/api/image-automation.md | 16 ++ docs/spec/v1beta1/imageupdateautomations.md | 34 +++- .../imageupdateautomation_controller.go | 152 +++++++++++------- internal/controller/update_test.go | 96 +++++++++-- 6 files changed, 242 insertions(+), 75 deletions(-) diff --git a/api/v1beta1/git.go b/api/v1beta1/git.go index a40c7d24..24132288 100644 --- a/api/v1beta1/git.go +++ b/api/v1beta1/git.go @@ -85,6 +85,13 @@ type PushSpec struct { // Branch specifies that commits should be pushed to the branch // named. The branch is created using `.spec.checkout.branch` as the // starting point, if it doesn't already exist. - // +required - Branch string `json:"branch"` + // +optional + Branch string `json:"branch,omitempty"` + + // Refspec specifies the Git Refspec to use for a push operation. + // It takes precedence over Branch, i.e. Branch is ignored + // if Refspec is non empty. For more details about Git Refspecs, see: + // https://git-scm.com/book/en/v2/Git-Internals-The-Refspec + // +optional + Refspec string `json:"refspec,omitempty"` } diff --git a/config/crd/bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml b/config/crd/bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml index dd6b3598..affdb72a 100644 --- a/config/crd/bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml +++ b/config/crd/bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml @@ -135,8 +135,12 @@ spec: to the branch named. The branch is created using `.spec.checkout.branch` as the starting point, if it doesn't already exist. type: string - required: - - branch + refspec: + description: 'Refspec specifies the Git Refspec to use for + a push operation. It takes precedence over Branch, i.e. + Branch is ignored if Refspec is non empty. For more details + about Git Refspecs, see: https://git-scm.com/book/en/v2/Git-Internals-The-Refspec' + type: string type: object required: - commit diff --git a/docs/api/image-automation.md b/docs/api/image-automation.md index b74558da..06bc7d0e 100644 --- a/docs/api/image-automation.md +++ b/docs/api/image-automation.md @@ -638,11 +638,27 @@ string +(Optional)

Branch specifies that commits should be pushed to the branch named. The branch is created using .spec.checkout.branch as the starting point, if it doesn’t already exist.

+ + +refspec
+ +string + + + +(Optional) +

Refspec specifies the Git Refspec to use for a push operation. +It takes precedence over Branch, i.e. Branch is ignored +if Refspec is non empty. For more details about Git Refspecs, see: +https://git-scm.com/book/en/v2/Git-Internals-The-Refspec

+ + diff --git a/docs/spec/v1beta1/imageupdateautomations.md b/docs/spec/v1beta1/imageupdateautomations.md index bbeb5dfe..006aa273 100644 --- a/docs/spec/v1beta1/imageupdateautomations.md +++ b/docs/spec/v1beta1/imageupdateautomations.md @@ -406,8 +406,15 @@ type PushSpec struct { // Branch specifies that commits should be pushed to the branch // named. The branch is created using `.spec.checkout.branch` as the // starting point, if it doesn't already exist. - // +required - Branch string `json:"branch"` + // +optional + Branch string `json:"branch,omitempty"` + + // Refspec specifies the Git Refspec to use for a push operation. + // It takes precedence over Branch, i.e. Branch is ignored + // if Refspec is non empty. For more details about Git Refspecs, see: + // https://git-scm.com/book/en/v2/Git-Internals-The-Refspec + // +optional + Refspec string `json:"refspec,omitempty"` } ``` @@ -416,7 +423,11 @@ pushed to the same branch at the origin. If `.spec.git.checkout` is not present, to the branch given in the `GitRepository` referenced by `.spec.sourceRef`. If none of these yield a branch name, the automation will fail. -When `push` is present, the `branch` field specifies a branch to push to at the origin. The branch +If `push.refspec` is present, the refspec specified is used to perform the push operation. +An example of a valid refspec is `refs/heads/branch:refs/heads/branch`. This allows users to push to an +arbitary destination reference. + +If `push.branch` is present, the specified branch is pushed to at the origin. The branch will be created locally if it does not already exist, starting from the checkout branch. If it does already exist, it will be overwritten with the cloned version plus the changes made by the controller. Alternatively, force push can be disabled by starting the controller with `--feature-gates=GitForcePushBranch=false`, @@ -425,6 +436,8 @@ Note that without force push in push branches, if the target branch is stale, th be able to conclude the operation and will consistently fail until the branch is either deleted or refreshed. +**Note:** If `push.refspec` is specified, then `push.branch` is ignored. + In the following snippet, updates will be pushed as commits to the branch `auto`, and when that branch does not exist at the origin, it will be created locally starting from the branch `main`, and pushed: @@ -439,6 +452,21 @@ spec: branch: auto ``` +In the following snippet, updates and commits will be made on the `main` branch locally +and then pushed to the `qa/main` branch. Note that, the `auto` branch specified in +`spec.git.push.branch` is ignored. + +```yaml +spec: + git: + checkout: + ref: + branch: main + push: + branch: auto + refspec: refs/heads/main:refs/heads/qa/main +``` + ## Update strategy The `.spec.update` field specifies how to carry out updates on the git repository. There is one diff --git a/internal/controller/imageupdateautomation_controller.go b/internal/controller/imageupdateautomation_controller.go index cf8b5516..2d363df6 100644 --- a/internal/controller/imageupdateautomation_controller.go +++ b/internal/controller/imageupdateautomation_controller.go @@ -214,30 +214,15 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr } // validate the git spec and default any values needed later, before proceeding - var ref *sourcev1.GitRepositoryRef + var checkoutRef *sourcev1.GitRepositoryRef if gitSpec.Checkout != nil { - ref = &gitSpec.Checkout.Reference - tracelog.Info("using git repository ref from .spec.git.checkout", "ref", ref) + checkoutRef = &gitSpec.Checkout.Reference + tracelog.Info("using git repository ref from .spec.git.checkout", "ref", checkoutRef) } else if r := origin.Spec.Reference; r != nil { - ref = r - tracelog.Info("using git repository ref from GitRepository spec", "ref", ref) + checkoutRef = r + tracelog.Info("using git repository ref from GitRepository spec", "ref", checkoutRef) } // else remain as `nil` and git.DefaultBranch will be used. - var pushBranch string - if gitSpec.Push != nil { - pushBranch = gitSpec.Push.Branch - tracelog.Info("using push branch from .spec.push.branch", "branch", pushBranch) - } else { - // Here's where it gets constrained. If there's no push branch - // given, then the checkout ref must include a branch, and - // that can be used. - if ref == nil || ref.Branch == "" { - return failWithError(fmt.Errorf("Push branch not given explicitly, and cannot be inferred from .spec.git.checkout.ref or GitRepository .spec.ref")) - } - pushBranch = ref.Branch - tracelog.Info("using push branch from $ref.branch", "branch", pushBranch) - } - tmp, err := os.MkdirTemp("", fmt.Sprintf("%s-%s", originName.Namespace, originName.Name)) if err != nil { return failWithError(err) @@ -248,42 +233,44 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr } }() - debuglog.Info("attempting to clone git repository", "gitrepository", originName, "ref", ref, "working", tmp) - - authOpts, err := r.getAuthOpts(ctx, &origin) - if err != nil { - return failWithError(err) - } - - clientOpts := []gogit.ClientOption{gogit.WithDiskStorage()} - if authOpts.Transport == git.HTTP { - clientOpts = append(clientOpts, gogit.WithInsecureCredentialsOverHTTP()) + var pushBranch string + var switchBranch bool + if gitSpec.Push != nil { + // We only need to switch branches when a branch has been specified in + // the push spec and a refspec has not. Furthermore, the branch needs to + // be different than the one in the checkout ref. + if gitSpec.Push.Branch != "" && gitSpec.Push.Branch != checkoutRef.Branch && gitSpec.Push.Refspec == "" { + pushBranch = gitSpec.Push.Branch + switchBranch = true + tracelog.Info("using push branch from .spec.push.branch", "branch", pushBranch) + } + } else { + // Here's where it gets constrained. If there's no push branch + // given, then the checkout ref must include a branch, and + // that can be used. + if checkoutRef == nil || checkoutRef.Branch == "" { + return failWithError( + fmt.Errorf("Push spec not provided, and cannot be inferred from .spec.git.checkout.ref or GitRepository .spec.ref"), + ) + } + pushBranch = checkoutRef.Branch + tracelog.Info("using push branch from $ref.branch", "branch", pushBranch) } - // If the push branch is different from the checkout ref, we need to - // have all the references downloaded at clone time, to ensure that - // SwitchBranch will have access to the target branch state. fluxcd/flux2#3384 - // - // To always overwrite the push branch, the feature gate - // GitAllBranchReferences can be set to false, which will cause - // the SwitchBranch operation to ignore the remote branch state. - allReferences := r.features[features.GitAllBranchReferences] - if pushBranch != ref.Branch { - clientOpts = append(clientOpts, gogit.WithSingleBranch(!allReferences)) - } + debuglog.Info("attempting to clone git repository", "gitrepository", originName, "ref", checkoutRef, "working", tmp) - gitClient, err := gogit.NewClient(tmp, authOpts, clientOpts...) + gitClient, err := r.constructGitClient(ctx, &origin, tmp, switchBranch) if err != nil { return failWithError(err) } defer gitClient.Close() opts := repository.CloneConfig{} - if ref != nil { - opts.Tag = ref.Tag - opts.SemVer = ref.SemVer - opts.Commit = ref.Commit - opts.Branch = ref.Branch + if checkoutRef != nil { + opts.Tag = checkoutRef.Tag + opts.SemVer = checkoutRef.SemVer + opts.Commit = checkoutRef.Commit + opts.Branch = checkoutRef.Branch } if enabled, _ := r.features[features.GitShallowClone]; enabled { @@ -297,9 +284,9 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr return failWithError(err) } - // When there's a push spec, the pushed-to branch is where commits + // When there's a push branch specified, the pushed-to branch is where commits // shall be made - if gitSpec.Push != nil && !(ref != nil && ref.Branch == pushBranch) { + if switchBranch { // Use the git operations timeout for the repo. fetchCtx, cancel := context.WithTimeout(ctx, origin.Spec.Timeout.Duration) defer cancel() @@ -352,7 +339,6 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr debuglog.Info("ran updates to working dir", "working", tmp) - var statusMessage string var signingEntity *openpgp.Entity if gitSpec.Commit.SigningKey != nil { if signingEntity, err = r.getSigningEntity(ctx, auto); err != nil { @@ -386,6 +372,7 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr err = extgogit.ErrEmptyCommit } + var statusMessage string if err != nil { if !errors.Is(err, git.ErrNoStagedFiles) && !errors.Is(err, extgogit.ErrEmptyCommit) { return failWithError(err) @@ -401,20 +388,39 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr // Use the git operations timeout for the repo. pushCtx, cancel := context.WithTimeout(ctx, origin.Spec.Timeout.Duration) defer cancel() - opts := repository.PushConfig{} + var pushConfig repository.PushConfig + + // Use push refspec if provided. + if gitSpec.Push != nil && gitSpec.Push.Refspec != "" { + pushConfig.Refspecs = []string{gitSpec.Push.Refspec} + } + + // If the force push feature flag is true and we are pushing to a + // different branch than the one we checked out to, then force push + // these changes. forcePush := r.features[features.GitForcePushBranch] - if forcePush && pushBranch != ref.Branch { - opts.Force = true + if forcePush && switchBranch { + pushConfig.Force = true } - if err := gitClient.Push(pushCtx, opts); err != nil { + + if err := gitClient.Push(pushCtx, pushConfig); err != nil { return failWithError(err) } - r.event(ctx, auto, eventv1.EventSeverityInfo, fmt.Sprintf("Committed and pushed change %s to %s\n%s", rev, pushBranch, message)) - log.Info("pushed commit to origin", "revision", rev, "branch", pushBranch) + if len(pushConfig.Refspecs) > 0 { + r.event(ctx, auto, eventv1.EventSeverityInfo, + fmt.Sprintf("Committed and pushed change %s using refspec %s\n%s", rev, gitSpec.Push.Refspec, message)) + log.Info("pushed commit to origin", "revision", rev, "refspec", gitSpec.Push.Refspec) + statusMessage = "committed and pushed " + rev + " using refspec " + gitSpec.Push.Refspec + } else { + r.event(ctx, auto, eventv1.EventSeverityInfo, + fmt.Sprintf("Committed and pushed change %s to %s\n%s", rev, pushBranch, message)) + log.Info("pushed commit to origin", "revision", rev, "branch", pushBranch) + statusMessage = "committed and pushed " + rev + " to " + pushBranch + } + auto.Status.LastPushCommit = rev auto.Status.LastPushTime = &metav1.Time{Time: start} - statusMessage = "committed and pushed " + rev + " to " + pushBranch } // Getting to here is a successful run. @@ -545,6 +551,38 @@ func (r *ImageUpdateAutomationReconciler) getAuthOpts(ctx context.Context, repos return opts, nil } +// constructGitClient constructs and returns a new gogit client. +func (r *ImageUpdateAutomationReconciler) constructGitClient(ctx context.Context, + origin *sourcev1.GitRepository, repoDir string, switchBranch bool) (*gogit.Client, error) { + authOpts, err := r.getAuthOpts(ctx, origin) + if err != nil { + return nil, err + } + + clientOpts := []gogit.ClientOption{gogit.WithDiskStorage()} + if authOpts.Transport == git.HTTP { + clientOpts = append(clientOpts, gogit.WithInsecureCredentialsOverHTTP()) + } + + // If the push branch is different from the checkout ref, we need to + // have all the references downloaded at clone time, to ensure that + // SwitchBranch will have access to the target branch state. fluxcd/flux2#3384 + // + // To always overwrite the push branch, the feature gate + // GitAllBranchReferences can be set to false, which will cause + // the SwitchBranch operation to ignore the remote branch state. + allReferences := r.features[features.GitAllBranchReferences] + if switchBranch { + clientOpts = append(clientOpts, gogit.WithSingleBranch(!allReferences)) + } + + gitClient, err := gogit.NewClient(repoDir, authOpts, clientOpts...) + if err != nil { + return nil, err + } + return gitClient, nil +} + // getSigningEntity retrieves an OpenPGP entity referenced by the // provided imagev1.ImageUpdateAutomation for git commit signing func (r *ImageUpdateAutomationReconciler) getSigningEntity(ctx context.Context, auto imagev1.ImageUpdateAutomation) (*openpgp.Entity, error) { diff --git a/internal/controller/update_test.go b/internal/controller/update_test.go index 5597a76f..9697b0f7 100644 --- a/internal/controller/update_test.go +++ b/internal/controller/update_test.go @@ -168,7 +168,7 @@ func TestImageAutomationReconciler_commitMessage(t *testing.T) { updateStrategy := &imagev1.UpdateStrategy{ Strategy: imagev1.UpdateStrategySetters, } - err := createImageUpdateAutomation(testEnv, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, "", testCommitTemplate, "", updateStrategy) + err := createImageUpdateAutomation(testEnv, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, "", "", testCommitTemplate, "", updateStrategy) g.Expect(err).ToNot(HaveOccurred()) // Wait for a new commit to be made by the controller. @@ -235,7 +235,7 @@ func TestImageAutomationReconciler_crossNamespaceRef(t *testing.T) { updateStrategy := &imagev1.UpdateStrategy{ Strategy: imagev1.UpdateStrategySetters, } - err := createImageUpdateAutomation(testEnv, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, "", testCommitTemplate, "", updateStrategy) + err := createImageUpdateAutomation(testEnv, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, "", "", testCommitTemplate, "", updateStrategy) g.Expect(err).ToNot(HaveOccurred()) // Wait for a new commit to be made by the controller. @@ -270,7 +270,7 @@ func TestImageAutomationReconciler_crossNamespaceRef(t *testing.T) { updateStrategy := &imagev1.UpdateStrategy{ Strategy: imagev1.UpdateStrategySetters, } - err := createImageUpdateAutomation(r.Client, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, "", testCommitTemplate, "", updateStrategy) + err := createImageUpdateAutomation(r.Client, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, "", "", testCommitTemplate, "", updateStrategy) g.Expect(err).ToNot(HaveOccurred()) imageUpdateKey := types.NamespacedName{ @@ -336,7 +336,7 @@ func TestImageAutomationReconciler_updatePath(t *testing.T) { Strategy: imagev1.UpdateStrategySetters, Path: "./yes", } - err := createImageUpdateAutomation(testEnv, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, "", testCommitTemplate, "", updateStrategy) + err := createImageUpdateAutomation(testEnv, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, "", "", testCommitTemplate, "", updateStrategy) g.Expect(err).ToNot(HaveOccurred()) // Wait for a new commit to be made by the controller. @@ -395,7 +395,7 @@ func TestImageAutomationReconciler_signedCommit(t *testing.T) { updateStrategy := &imagev1.UpdateStrategy{ Strategy: imagev1.UpdateStrategySetters, } - err = createImageUpdateAutomation(testEnv, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, "", testCommitTemplate, signingKeySecretName, updateStrategy) + err = createImageUpdateAutomation(testEnv, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, "", "", testCommitTemplate, signingKeySecretName, updateStrategy) g.Expect(err).ToNot(HaveOccurred()) // Wait for a new commit to be made by the controller. @@ -425,6 +425,58 @@ func TestImageAutomationReconciler_signedCommit(t *testing.T) { }) } +func TestImageAutomationReconciler_push_refspec(t *testing.T) { + policySpec := imagev1_reflect.ImagePolicySpec{ + ImageRepositoryRef: meta.NamespacedObjectReference{ + Name: "not-expected-to-exist", + }, + Policy: imagev1_reflect.ImagePolicyChoice{ + SemVer: &imagev1_reflect.SemVerPolicy{ + Range: "1.x", + }, + }, + } + fixture := "testdata/appconfig" + latest := "helloworld:v1.0.0" + + t.Run(gogit.ClientName, func(t *testing.T) { + testWithRepoAndImagePolicy( + NewWithT(t), testEnv, fixture, policySpec, latest, gogit.ClientName, + func(g *WithT, s repoAndPolicyArgs, repoURL string, localRepo *extgogit.Repository) { + // Update the setter marker in the repo. + policyKey := types.NamespacedName{ + Name: s.imagePolicyName, + Namespace: s.namespace, + } + commitInRepo(g, repoURL, s.branch, "Install setter marker", func(tmp string) { + g.Expect(replaceMarker(tmp, policyKey)).To(Succeed()) + }) + preChangeCommitId := commitIdFromBranch(localRepo, s.branch) + + // Pull the head commit that was just pushed, so it's not considered a new + // commit when checking for a commit made by automation. + waitForNewHead(g, localRepo, s.branch, preChangeCommitId) + preChangeCommitId = commitIdFromBranch(localRepo, s.branch) + + // Create the automation object and let it make a commit itself. + updateStrategy := &imagev1.UpdateStrategy{ + Strategy: imagev1.UpdateStrategySetters, + } + refspec := fmt.Sprintf("refs/heads/%s:refs/heads/smth/else", s.branch) + err := createImageUpdateAutomation(testEnv, "push-refspec", s.namespace, + s.gitRepoName, s.gitRepoNamespace, s.branch, "shouldBeIgnored", refspec, + testCommitTemplate, "", updateStrategy) + g.Expect(err).ToNot(HaveOccurred()) + + // Wait for a new commit to be made by the controller to the destination + // ref specified in refspec (the stuff after the colon). + hash := getRemoteRef(g, repoURL, "smth/else") + g.Expect(hash.String()).ToNot(Equal(preChangeCommitId)) + }, + ) + }) +} + func TestImageAutomationReconciler_e2e(t *testing.T) { protos := []string{"http", "ssh"} @@ -547,7 +599,7 @@ func TestImageAutomationReconciler_e2e(t *testing.T) { // Now create the automation object, and let it (one // hopes!) make a commit itself. - err = createImageUpdateAutomation(r.Client, imageUpdateAutomationName, namespace, gitRepoName, namespace, branch, pushBranch, commitMessage, "", updateStrategy) + err = createImageUpdateAutomation(r.Client, imageUpdateAutomationName, namespace, gitRepoName, namespace, branch, pushBranch, "", commitMessage, "", updateStrategy) g.Expect(err).ToNot(HaveOccurred()) _, err = r.Reconcile(context.TODO(), ctrl.Request{NamespacedName: automationKey}) @@ -712,7 +764,7 @@ func TestImageAutomationReconciler_e2e(t *testing.T) { Namespace: namespace, Name: "update-" + randStringRunes(5), } - err = createImageUpdateAutomation(r.Client, updateKey.Name, namespace, gitRepoName, namespace, branch, "", commitMessage, "", updateStrategy) + err = createImageUpdateAutomation(r.Client, updateKey.Name, namespace, gitRepoName, namespace, branch, "", "", commitMessage, "", updateStrategy) g.Expect(err).ToNot(HaveOccurred()) defer func() { g.Expect(deleteImageUpdateAutomation(r.Client, updateKey.Name, namespace)).To(Succeed()) @@ -762,7 +814,7 @@ func TestImageAutomationReconciler_e2e(t *testing.T) { Namespace: namespace, Name: "update-" + randStringRunes(5), } - err = createImageUpdateAutomation(testEnv, updateKey.Name, namespace, gitRepoName, namespace, branch, "", commitMessage, "", updateStrategy) + err = createImageUpdateAutomation(testEnv, updateKey.Name, namespace, gitRepoName, namespace, branch, "", "", commitMessage, "", updateStrategy) g.Expect(err).ToNot(HaveOccurred()) defer func() { g.Expect(deleteImageUpdateAutomation(testEnv, updateKey.Name, namespace)).To(Succeed()) @@ -1214,6 +1266,27 @@ func clone(ctx context.Context, repoURL, branchName string) (*extgogit.Repositor return repo, nil } +func getRemoteRef(g *WithT, repoURL, ref string) plumbing.Hash { + var hash plumbing.Hash + g.Eventually(func() bool { + cloneCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + repo, err := clone(cloneCtx, repoURL, ref) + if err != nil { + return false + } + + remRefName := plumbing.NewRemoteReferenceName(extgogit.DefaultRemoteName, ref) + remRef, err := repo.Reference(remRefName, true) + if err != nil { + return false + } + hash = remRef.Hash() + return true + }, timeout, time.Second).Should(BeTrue()) + return hash +} + func waitForNewHead(g *WithT, repo *extgogit.Repository, branch, preChangeHash string) { var commitToResetTo *object.Commit @@ -1523,7 +1596,7 @@ func updateImagePolicyWithLatestImage(kClient client.Client, name, namespace, la } func createImageUpdateAutomation(kClient client.Client, name, namespace, - gitRepo, gitRepoNamespace, checkoutBranch, pushBranch, commitTemplate, signingKeyRef string, + gitRepo, gitRepoNamespace, checkoutBranch, pushBranch, pushRefspec, commitTemplate, signingKeyRef string, updateStrategy *imagev1.UpdateStrategy) error { updateAutomation := &imagev1.ImageUpdateAutomation{ Spec: imagev1.ImageUpdateAutomationSpec{ @@ -1552,9 +1625,10 @@ func createImageUpdateAutomation(kClient client.Client, name, namespace, } updateAutomation.Name = name updateAutomation.Namespace = namespace - if pushBranch != "" { + if pushRefspec != "" || pushBranch != "" { updateAutomation.Spec.GitSpec.Push = &imagev1.PushSpec{ - Branch: pushBranch, + Refspec: pushRefspec, + Branch: pushBranch, } } if signingKeyRef != "" {