Skip to content
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
cb32742
update git pusher config schema
fuskovic Feb 20, 2026
a114d13
update config validation tests
fuskovic Feb 20, 2026
56b8fec
support pushing tags
fuskovic Feb 20, 2026
28a5928
Merge branch 'main' into fuskovic/git-tag
fuskovic Feb 24, 2026
777eb14
change non-tag case to default
fuskovic Feb 24, 2026
8850aff
update git push config schema comments
fuskovic Feb 24, 2026
5978215
update intro and add note to git-push step doc
fuskovic Feb 24, 2026
e60cdc9
codegen
fuskovic Feb 24, 2026
cbbee61
add Pushing tags example to git-push doc and place holder todo doc fo…
fuskovic Feb 24, 2026
3808ae3
implement test suite for git pusher for testing various control flows…
fuskovic Feb 24, 2026
564f0fe
git push tag test passing
fuskovic Feb 24, 2026
70652bf
git push tag test passing
fuskovic Feb 24, 2026
9e415e1
DRY refactor and fix lint issues
fuskovic Feb 25, 2026
2a520e8
add t.helper to test suite helpers
fuskovic Feb 25, 2026
556e7f7
parallelize git pusher tests
fuskovic Feb 25, 2026
a69bb9c
protect against panic
fuskovic Feb 25, 2026
c401a07
add git tag config schema + make codegen
fuskovic Feb 25, 2026
ea0e2bb
implement git tag step
fuskovic Feb 25, 2026
6a26e40
add tests for git tag step
fuskovic Feb 25, 2026
ca59651
add git-tag doc
fuskovic Feb 25, 2026
8435eb3
lint
fuskovic Feb 25, 2026
ac84f41
fix schema config constraint + improve error message
fuskovic Feb 26, 2026
c7df4da
fix: revert #5767 (#5795)
krancour Feb 25, 2026
ee5a4a9
Merge branch 'main' into fuskovic/git-tag
krancour Feb 26, 2026
6789a7b
Apply suggestions from code review
fuskovic Feb 27, 2026
3fceaca
keep original git push test convention
fuskovic Feb 27, 2026
4f515eb
verify tag was created using workTree.Checkout
fuskovic Feb 27, 2026
52d7630
make codegen
fuskovic Feb 27, 2026
2a97b89
update comment to reflect PullRebase=true has no effect when Tag is set
fuskovic Feb 27, 2026
0ec7081
move rebase logic into default case
fuskovic Feb 27, 2026
64efdab
include the commit the tag points to in the git-tag output
fuskovic Feb 27, 2026
7acaef5
conditionally set the tag key in git push output
fuskovic Feb 27, 2026
cd543a9
remove tag key in output var declaration
fuskovic Feb 27, 2026
f9cac23
add assertions for commit data in git push tag test
fuskovic Feb 27, 2026
b002dc5
dynamically provision output data
fuskovic Feb 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ description: Pushes the committed changes in a specified working tree to a speci

# `git-push`

`git-push` pushes the committed changes in a specified working tree to a
specified branch in the remote repository. This step typically follows a
[`git-commit` step](git-commit.md) and is often followed by a
`git-push` can either push committed changes in a specified working tree to a
specified branch or a new tag to the remote repository. This step typically
follows a [`git-commit` step](git-commit.md) and is often followed by a
[`git-open-pr` step](git-open-pr.md).

This step also implements its own, internal retry logic. If a push fails, with
Expand All @@ -28,14 +28,24 @@ Stages that write to the same branch do not write to the same files.

:::


:::note

For a tag push, an attempt to pull/rebase first is not made as tags are
immutable and any existing tag with the same name on the remote would cause
the pull/rebase to fail.

:::

## Configuration

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `path` | `string` | Y | Path to a Git working tree containing committed changes. |
| `targetBranch` | `string` | N | The branch to push to in the remote repository. Mutually exclusive with `generateTargetBranch=true`. If neither of these is provided, the target branch will be the same as the branch currently checked out in the working tree. |
| `targetBranch` | `string` | N | The branch to push to in the remote repository. Mutually exclusive with `generateTargetBranch=true` and `tag`. If neither of these is provided, the target branch will be the same as the branch currently checked out in the working tree. |
| `maxAttempts` | `int32` | N | The maximum number of attempts to make when pushing to the remote repository. Default is 50. |
| `generateTargetBranch` | `boolean` | N | Whether to push to a remote branch named like `kargo/promotion/<promotionName>`. If such a branch does not already exist, it will be created. A value of 'true' is mutually exclusive with `targetBranch`. If neither of these is provided, the target branch will be the currently checked out branch. This option is useful when a subsequent promotion step will open a pull request against a Stage-specific branch. In such a case, the generated target branch pushed to by the `git-push` step can later be utilized as the source branch of the pull request. |
| `generateTargetBranch` | `boolean` | N | Whether to push to a remote branch named like `kargo/promotion/<promotionName>`. If such a branch does not already exist, it will be created. A value of 'true' is mutually exclusive with `targetBranch` and `tag`. If neither of these is provided, the target branch will be the currently checked out branch. This option is useful when a subsequent promotion step will open a pull request against a Stage-specific branch. In such a case, the generated target branch pushed to by the `git-push` step can later be utilized as the source branch of the pull request. |
| `tag` | `string` | N | An optional tag to push to the remote repository. Mutually exclusive with `generateTargetBranch` `targetBranch`. |
| `force` | `boolean` | N | Whether to force push to the target branch, overwriting any existing history. This is useful for scenarios where you want to completely replace the branch content (e.g., pushing rendered manifests that don't depend on previous state). **Use with caution** as this will overwrite any commits that exist on the remote branch but not in your local branch. Default is `false`. |
| `provider` | `string` | N | The name of the Git provider to use. Currently 'azure', 'bitbucket', 'gitea', 'github', and 'gitlab' are supported. Kargo will try to infer the provider if it is not explicitly specified. This setting does not affect the push operation but helps generate the correct [`commitURL` output](#output) when working with repositories where the provider cannot be automatically determined, such as self-hosted instances. |

Expand All @@ -46,6 +56,7 @@ Stages that write to the same branch do not write to the same files.
| `branch` | `string` | The name of the remote branch pushed to by this step. This is especially useful when the `generateTargetBranch=true` option has been used, in which case a subsequent [`git-open-pr`](git-open-pr.md) will typically reference this output to learn what branch to use as the head branch of a new pull request. |
| `commit` | `string` | The ID (SHA) of the commit pushed by this step. |
| `commitURL` | `string` | The URL of the commit that was pushed to the remote repository. |
| `tag` | `string` | The tag that was pushed to the remote repository. |

## Examples

Expand Down Expand Up @@ -97,3 +108,20 @@ steps:
generateTargetBranch: true
# Open a PR and wait for it to be merged or closed...
```

### Pushing Tags

In this example, a tag is pushed to the remote repository. The `git-tag` step
typically precedes the [`git-tag`](git-tag.md) in order to create the new tag.

```yaml
# Create a new tag
- uses: git-tag
config:
path: ./out
tag: v1.0.0
- uses: git-push
config:
path: ./out
tag: v1.0.0
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
sidebar_label: git-tag
description: Creates a new tag for the latest committed changes.
---

# `git-tag`

The `git-tag` step creates a new tag in a local Git repository. This step is commonly used to mark specific commits with a tag, which can be useful for versioning or tracking changes in a repository.

## Configuration

| Name | Type | Required | Description |
|--------|----------|----------|-----------------------------------------------------------------------------|
| `path` | `string` | Y | Path to the local Git repository where the tag should be created. This path is relative to the temporary workspace that Kargo provisions for use by the promotion process. |
| `tag` | `string` | Y | The name of the tag to create. |

## Output

| Name | Type | Description |
|-------|----------|-----------------------------------------------------------------------------|
| `tag` | `string` | The name of the tag that was created by this step. This can be referenced in subsequent steps. |

:::caution

If the specified tag already exists, the git-tag step will fail.

:::

## Examples

### Basic Usage

In this example, the `git-tag` step creates a tag named `v1.0.0` in the local Git repository located at `./out`.

```yaml
steps:
- uses: git-tag
config:
path: ./out
tag: v1.0.0
```

### Tagging After a Commit

This example demonstrates how to use the git-tag step after a git-commit step to tag the latest commit with a version number.

```yaml
steps:
- uses: git-commit
config:
path: ./out
message: "Committing changes for release v1.0.0"
- uses: git-tag
config:
path: ./out
tag: v1.0.0
```

### Pushing After Tagging

In this example, the `git-tag` step creates a tag, and the `git-push` step pushes the tag to the remote repository.

```yaml
steps:
- uses: git-tag
config:
path: ./out
tag: v1.0.0
- uses: git-push
config:
path: ./out
tag: v1.0.0
```
5 changes: 5 additions & 0 deletions pkg/controller/git/mock_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type MockRepo struct {
CommitFn func(message string, opts *CommitOptions) error
CreateChildBranchFn func(branch string) error
CreateOrphanedBranchFn func(branch string) error
CreateTagFn func(tag string) error
CurrentBranchFn func() (string, error)
DeleteBranchFn func(branch string) error
DirFn func() string
Expand Down Expand Up @@ -75,6 +76,10 @@ func (m *MockRepo) CreateOrphanedBranch(branch string) error {
return m.CreateOrphanedBranchFn(branch)
}

func (m *MockRepo) CreateTag(tag string) error {
return m.CreateTagFn(tag)
}

func (m *MockRepo) CurrentBranch() (string, error) {
return m.CurrentBranchFn()
}
Expand Down
31 changes: 26 additions & 5 deletions pkg/controller/git/work_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type WorkTree interface {
// CreateOrphanedBranch creates a new branch that shares no commit history
// with any other branch.
CreateOrphanedBranch(branch string) error
// CreateTag creates a new tag.
CreateTag(tag string) error
// CurrentBranch returns the current branch
CurrentBranch() (string, error)
// DeleteBranch deletes the specified branch
Expand Down Expand Up @@ -296,6 +298,13 @@ func (w *workTree) CreateOrphanedBranch(branch string) error {
return w.Clean()
}

func (w *workTree) CreateTag(tag string) error {
if _, err := libExec.Exec(w.buildGitCommand("tag", tag)); err != nil {
return fmt.Errorf("error creating tag %q", err)
}
return nil
}

func (w *workTree) CurrentBranch() (string, error) {
res, err := libExec.Exec(w.buildGitCommand("branch", "--show-current"))
if err != nil {
Expand Down Expand Up @@ -584,6 +593,9 @@ type PushOptions struct {
// be useful when pushing changes to a remote branch that has been updated
// in the time since the local branch was last pulled.
PullRebase bool
// Tag specifies an optional tag to push to the remote repository. Mutually
// exclusive with TargetBranch.
Tag string
}

// https://regex101.com/r/f7kTjs/1
Expand All @@ -596,13 +608,13 @@ func (w *workTree) Push(opts *PushOptions) error {
opts = &PushOptions{}
}
targetBranch := opts.TargetBranch
if targetBranch == "" {
if targetBranch == "" && opts.Tag == "" {
var err error
if targetBranch, err = w.CurrentBranch(); err != nil {
return err
}
}
if opts.PullRebase {
if opts.PullRebase && targetBranch != "" {
exists, err := w.RemoteBranchExists(targetBranch)
if err != nil {
return err
Expand All @@ -622,15 +634,24 @@ func (w *workTree) Push(opts *PushOptions) error {
}
}
}
args := []string{"push", "origin", fmt.Sprintf("HEAD:%s", targetBranch)}
var artifact string
args := []string{"push", "origin"}
switch {
case opts.Tag != "":
artifact = "tag"
args = append(args, "tag", opts.Tag)
default:
artifact = "branch"
args = append(args, fmt.Sprintf("HEAD:%s", targetBranch))
}
if opts.Force {
args = append(args, "--force")
}
if res, err := libExec.Exec(w.buildGitCommand(args...)); err != nil {
if nonFastForwardRegex.MatchString(string(res)) {
return fmt.Errorf("error pushing branch: %w", ErrNonFastForward)
return fmt.Errorf("error pushing %s: %w", artifact, ErrNonFastForward)
}
return fmt.Errorf("error pushing branch: %w", err)
return fmt.Errorf("error pushing %s: %w", artifact, err)
}
return nil
}
Expand Down
6 changes: 4 additions & 2 deletions pkg/gitprovider/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ func New(repoURL string, opts *Options) (Interface, error) {
return nil, fmt.Errorf("no registered providers with name %q", opts.Name)
}
for _, reg := range registeredProviders {
if reg.Predicate(repoURL) {
return reg.NewProvider(repoURL, opts)
if reg.Predicate != nil && reg.NewProvider != nil {
if reg.Predicate(repoURL) {
return reg.NewProvider(repoURL, opts)
}
Comment on lines +36 to +39
Copy link
Member

Choose a reason for hiding this comment

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

Total aside and note to self... I realize this code wasn't ever refactored to use the PredicateBasedRegistry. Out of scope for this PR, but I should fix that.

}
}
return nil, fmt.Errorf("no registered providers for %s", repoURL)
Expand Down
14 changes: 12 additions & 2 deletions pkg/promotion/runner/builtin/git_pusher.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ const (
// stateKeyCommitURL is the key used to store the URL of the commit that was
// pushed to in the shared State.
stateKeyCommitURL = "commitURL"

// stateKeyCommit is the key used to store the tag that was pushed in the shared State.
stateKeyTag = "tag"
)

func init() {
Expand All @@ -47,7 +50,7 @@ func init() {
}

// gitPushPusher is an implementation of the promotion.StepRunner interface that
// pushes commits from a local Git repository to a remote Git repository.
// pushes commits and tags from a local Git repository to a remote Git repository.
type gitPushPusher struct {
schemaLoader gojsonschema.JSONLoader
credsDB credentials.Database
Expand Down Expand Up @@ -154,7 +157,13 @@ func (g *gitPushPusher) run(
// branch is specific to this Promotion only holds, it is also safe to do this.
pushOpts.Force = true
}

if cfg.Tag != "" {
pushOpts.Tag = cfg.Tag
// If we're pushing a tag, we should not attempt to pull/rebase first as
// tags are immutable and any existing tag with the same name on the remote
// would cause the pull/rebase to fail.
pushOpts.PullRebase = false
}
// Disable pull/rebase when force pushing to allow overwriting remote history
if pushOpts.Force {
pushOpts.PullRebase = false
Expand Down Expand Up @@ -238,6 +247,7 @@ func (g *gitPushPusher) run(
stateKeyBranch: pushOpts.TargetBranch,
stateKeyCommit: commitID,
stateKeyCommitURL: commitURL,
stateKeyTag: pushOpts.Tag,
},
}, nil
}
Expand Down
Loading