Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
09e0816
feat: add azure devops support
olblak Apr 24, 2026
f63c5b8
feat: add azuredevops e2e test
olblak Apr 25, 2026
5cf04ae
chore: add organization parameter
olblak Apr 26, 2026
346544b
fix: align azure devops pr body with bitbucket
olblak Apr 26, 2026
be21b62
fix: add missing test
olblak Apr 26, 2026
e54a07a
fix: add missing azuredevops policy scaffold asset
olblak Apr 26, 2026
18c7a65
feat: add azuredevopssearch to update multiple repositories
olblak Apr 26, 2026
fd0a993
Merge branch 'main' into issue/7090
olblak Apr 26, 2026
6afc516
Merge branch 'main' into issue/7090
olblak Apr 26, 2026
b22f5aa
Merge branch 'main' into issue/7090
olblak Apr 27, 2026
0019d6b
chore: go mod tidy
olblak Apr 27, 2026
aec2766
chore: make projet and repository optional
olblak Apr 27, 2026
0a0e665
fix: correctly restore unsupportedCapabilities
olblak Apr 27, 2026
a8e6040
feat: support reset pullrequest description
olblak Apr 27, 2026
86baf5d
Merge branch 'main' into issue/7090
olblak May 2, 2026
eed6796
feat: allow to use global env variable to auth on azure devops
olblak May 6, 2026
69bf0fa
fix: azuredevops/pullrequest test
olblak May 6, 2026
526c60a
Merge branch 'main' into issue/7090
olblak May 6, 2026
c9af8d8
fix: e2e pipeline title
olblak May 6, 2026
2512001
Merge branch 'issue/7090' of github.com:olblak/updatecli into issue/7090
olblak May 6, 2026
58ac567
fix: git capabilities reset
olblak May 6, 2026
21c6f03
feat: allow azuredevopssearch to search all project by default
olblak May 6, 2026
0708d26
fix: use filtercontain
olblak May 6, 2026
ae41582
fix: add missing organisation in scaffold config
olblak May 6, 2026
72a0f48
Merge branch 'main' into issue/7090
olblak May 10, 2026
f533cb4
Merge branch 'main' into issue/7090
olblak May 10, 2026
8002126
doc: update README.md to mention scm
olblak May 11, 2026
bb68d4f
Merge branch 'issue/7090' of github.com:olblak/updatecli into issue/7090
olblak May 11, 2026
c9bebcc
doc: remove toc
olblak May 11, 2026
5360368
fix: e2e test
olblak May 11, 2026
0af38bc
Merge branch 'main' into issue/7090
olblak May 11, 2026
62519fd
Fix e2e test to ignore warning: content
olblak May 12, 2026
a5da238
Merge branch 'issue/7090' of github.com:olblak/updatecli into issue/7090
olblak May 12, 2026
a226571
fix: azurededvopssearch default env credentials
olblak May 13, 2026
b36486b
feat: add missing scaffold azuredevopssearch scm
olblak May 13, 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
103 changes: 91 additions & 12 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,101 @@ link:https://img.shields.io/github/actions/workflow/status/updatecli/updatecli/g
link:https://api.securityscorecards.dev/projects/github.com/updatecli/updatecli[image:https://api.securityscorecards.dev/projects/github.com/updatecli/updatecli/badge[OpenSSF Scorecard]]
link:https://bestpractices.coreinfrastructure.org/projects/6731[image:https://bestpractices.coreinfrastructure.org/projects/6731/badge[OpenSSF Best Practices]]

_"Automatically open a PR on your GitOps repository when a third party service publishes an update"_
_"Automatically open a PR on your Git repository when a file update is needed"_

Updatecli is a tool used to apply file update strategies. Designed to be used from everywhere, each application "run" detects if a value needs to be updated using a custom strategy then apply changes according to the strategy.
Updatecli is a universal declarative update policy engine. Designed to be used from everywhere, each Updatecli "run" detects if a file needs to be updated using a tailored update policy then apply changes.

You describe your update strategy in a file then you run updatecli to it.
You describe your update strategy in a policy then you enforce it using Updatecli.

Updatecli reads a yaml or a go template configuration file, then works into three stages
Every Updatecli policy is a YAML (or Go template) file that runs through three stages:

1. Sources: Based on a rule, updatecli fetches a value that will be injected in later stages such as latest application version.
2. Conditions: Ensure that conditions are met based on the value retrieved during the source stage.
3. Targets: Update and publish the target files based on a value retrieved from the source stage.
1. *Sources* — Fetch the new value to apply (e.g. latest Docker image tag, newest Helm chart version, latest GitHub release).
2. *Conditions* — Verify that all prerequisites are met before making any change (optional but recommended).
3. *Targets* — Apply the change to the right file or service, and open a pull request if an SCM is configured.

== What Can You Update?

Updatecli ships with 30+ built-in integrations. Here are the most common scenarios:

* 📄 *File formats* —
Update values in YAML, JSON, TOML, XML, HCL, CSV, Dockerfiles, and `.tool-versions` files,
or any text file with pattern matching.

* 🐳 *Container images* —
Track Docker image tags and digests from Docker Hub or any OCI-compliant registry.

* 📦 *Package registries* —
Helm charts (including OCI), npm, PyPI, Maven, Cargo (Rust crates), Go modules,
and Terraform providers/modules.

* 🏷️ *Git & releases* —
GitHub/GitLab releases, Git tags and branches.

* ☕ *Languages & runtimes* —
Jenkins LTS/weekly releases, Eclipse Temurin (JDK), Go language versions,
Bazel modules and registry.

* ☁️ *Cloud* —
AWS AMIs.

* 🔧 *Custom logic* —
Shell scripts and HTTP endpoints, for anything not covered above.

Find the full list and documentation on link:https://www.updatecli.io/docs/prologue/introduction/[www.updatecli.io].

== Feature

* *Flexibility*: Easy to define tailored update strategies, you are just one yaml file from it.
* *Portability*: Easy to add to your workflow whatever it is. Just one command to run. Of course, it's easy to remove.
* *Extensibility*: Easy to add new go packages to support more workflows.
[cols="1,1,3,1", options="header"]
|===
| SCM Platform | Supported | Capabilities | Plugin

| GitHub
| ✅
| Clone, branch, commit, push, Pull Requests, releases
| `github`

| GitLab
| ✅
| Clone, branch, commit, push, Merge Requests, releases
| `gitlab`

| Gitea
| ✅
| Clone, branch, commit, push, Pull Requests, releases
| `gitea`

| Forgejo
| ✅
| Compatible through Gitea API support
| `gitea`

| Bitbucket Cloud
| ✅
| Clone, branch, commit, push, Pull Requests
| `bitbucket`

| Bitbucket Server / Stash
| ✅
| Clone, branch, commit, push, Pull Requests
| `stash`

| Azure DevOps
| ✅
| Clone, branch, commit, push, Pull Requests
| `azuredevops`

| Generic Git Repository
| ✅
| Clone, branch, commit, push
| `git`

|===

* *Declarative* — Define your update policy once in YAML; Updatecli handles detection and application.
* *30+ integrations* — Docker, Helm, GitHub releases, npm, PyPI, Terraform, AWS AMIs, and more — out of the box.
* *Any CI/CD* — Runs as a single binary. Drop it into GitHub Actions, Jenkins, GitLab CI, or any shell.
* *Safe by default* — Use `--dry-run` to preview every change before it is applied.
* *Extensible* — Add custom logic via shell scripts or HTTP, or contribute a new Go plugin.

== Why

Expand Down Expand Up @@ -213,10 +291,11 @@ More information available at link:https://github.com/updatecli/updatecli/blob/m
* link:https://www.updatecli.io/docs/prologue/introduction/[DOCUMENTATION]
* link:https://github.com/updatecli/updatecli/blob/main/LICENSE[LICENSE]

== Thanks to all the contributors ❤️
== Thanks to you
=== Contributors ❤️

link:https://github.com/updatecli/updatecli/graphs/contributors"[image:https://contrib.rocks/image?repo=updatecli/updatecli[]]

== Thanks to the sponsors ❤️
=== Sponsors ❤️

link:https://www.prefect.io/[image:https://avatars.githubusercontent.com/u/41086007?v=4[alt=PrefectHQ,height=200, width=200]]
44 changes: 44 additions & 0 deletions e2e/updatecli.d/success.d/azuredevops/scm.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Test Azure DevOps scm
pipelineid: azuredevops/scm

scms:
azuredevops:
kind: azuredevops
spec:
Comment thread
olblak marked this conversation as resolved.
organization: updatecli
project: "updatecli-test"
repository: "updatecli-test"
branch: main

sources:
license:
name: Retrieve license file content
kind: file
scmid: azuredevops
spec:
file: LICENSE

conditions:
license:
name: Retrieve license file content
kind: file
sourceid: license
scmid: azuredevops
spec:
file: LICENSE

targets:
license:
name: Update license title
kind: file
disablesourceinput: true
scmid: azuredevops
spec:
file: LICENSE
content: "# Title"
line: 1

actions:
default:
kind: azuredevops/pullrequest
scmid: azuredevops
28 changes: 28 additions & 0 deletions e2e/updatecli.d/success.d/azuredevopssearch/scm.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Test Azure Devops scm
pipelineid: azuredevops/scm

scms:
azuredevops:
kind: azuredevopssearch
spec:
organization: updatecli
#project: "^updatecli-test$"
#repository: "^updatecli-test$"
branch: "^main$"

targets:
readme:
name: Test update
kind: yaml
disablesourceinput: true
scmid: azuredevops
spec:
file: .github/workflows/updatecli.yaml
key: "$.permissions.contents"
value: read

actions:
default:
title: Change readme title
kind: azuredevops/pullrequest
scmid: azuredevops
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ require (
github.com/invopop/jsonschema v0.14.0
github.com/jferrl/go-githubauth v1.6.0
github.com/joho/godotenv v1.5.1
github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0
github.com/minamijoyo/hcledit v0.2.17
github.com/minamijoyo/tfupdate v0.8.0
github.com/muesli/mango-cobra v1.3.0
Expand Down Expand Up @@ -277,7 +278,7 @@ require (
github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/google/uuid v1.6.0
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/gosuri/uitable v0.0.4 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
Expand Down Expand Up @@ -575,6 +576,8 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0 h1:mmJCWLe63QvybxhW1iBmQWEaCKdc4SKgALfTNZ+OphU=
github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0/go.mod h1:mDunUZ1IUJdJIRHvFb+LPBUtxe3AYB5MI6BMXNg8194=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/minamijoyo/hcledit v0.2.17 h1:5FFiUu8BtJeldqb8OhZ4fKoGZuH3oiW30AM0dHuUIPQ=
Expand Down
40 changes: 40 additions & 0 deletions pkg/core/engine/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"github.com/updatecli/updatecli/pkg/core/pipeline/scm"
"github.com/updatecli/updatecli/pkg/core/reports"
"github.com/updatecli/updatecli/pkg/core/result"
"github.com/updatecli/updatecli/pkg/plugins/scms/azuredevops"
"github.com/updatecli/updatecli/pkg/plugins/scms/azuredevopssearch"
"github.com/updatecli/updatecli/pkg/plugins/scms/github"
"github.com/updatecli/updatecli/pkg/plugins/scms/githubsearch"
"github.com/updatecli/updatecli/pkg/plugins/scms/gitlab"
Expand Down Expand Up @@ -174,6 +176,44 @@ func (e *Engine) LoadConfigurations() error {
logrus.Debugf("githubsearch scm %q added new pipeline configuration for repository %s/%s", scmID, spec.Owner, spec.Repository)
}

case azuredevopssearch.Kind:

logrus.Debugf("Processing azuredevopssearch scm %q for potential multiple repository discovery", scmID)

ctx := context.Background()
azdoSearchScms, err := azuredevopssearch.New(scmConfig.Spec)
if err != nil {
return fmt.Errorf("unable to instantiate azuredevopssearch scm %q: %w", scmID, err)
}
discoveredAzureDevOpsSCms, err := azdoSearchScms.ScmsGenerator(ctx)
if err != nil {
return fmt.Errorf("unable to generate scm specs for azuredevopssearch scm %q: %w", scmID, err)
}

if len(discoveredAzureDevOpsSCms) == 0 {
return fmt.Errorf("no scm discovered for azuredevopssearch scm %q", scmID)
}

scmConfig.Kind = azuredevops.Kind
scmConfig.Spec = discoveredAzureDevOpsSCms[0]

loadedConfigurations[i].Spec.SCMs[scmID] = scmConfig

for _, spec := range discoveredAzureDevOpsSCms[1:] {
newPipeline := loadedConfiguration

newPipeline.Spec.SCMs = make(map[string]scm.Config, len(loadedConfiguration.Spec.SCMs))
maps.Copy(newPipeline.Spec.SCMs, loadedConfiguration.Spec.SCMs)

newSCM := newPipeline.Spec.SCMs[scmID]
newSCM.Kind = azuredevops.Kind
newSCM.Spec = spec

newPipeline.Spec.SCMs[scmID] = newSCM

loadedConfigurations = append(loadedConfigurations, newPipeline)
}

case gitlabsearch.Kind:

logrus.Debugf("Processing gitlabsearch scm %q for potential multiple repository discovery", scmID)
Expand Down
43 changes: 33 additions & 10 deletions pkg/core/pipeline/action/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import (
"github.com/updatecli/updatecli/pkg/core/jsonschema"
"github.com/updatecli/updatecli/pkg/core/pipeline/scm"
"github.com/updatecli/updatecli/pkg/core/reports"
azuredevops "github.com/updatecli/updatecli/pkg/plugins/resources/azuredevops/pullrequest"
bitbucket "github.com/updatecli/updatecli/pkg/plugins/resources/bitbucket/pullrequest"
gitea "github.com/updatecli/updatecli/pkg/plugins/resources/gitea/pullrequest"
gitlab "github.com/updatecli/updatecli/pkg/plugins/resources/gitlab/mergerequest"
stash "github.com/updatecli/updatecli/pkg/plugins/resources/stash/pullrequest"
azuredevopsscm "github.com/updatecli/updatecli/pkg/plugins/scms/azuredevops"
bitbucketscm "github.com/updatecli/updatecli/pkg/plugins/scms/bitbucket"
giteascm "github.com/updatecli/updatecli/pkg/plugins/scms/gitea"
"github.com/updatecli/updatecli/pkg/plugins/scms/github"
Expand All @@ -24,11 +26,12 @@ import (
)

const (
gitlabIdentifier = "gitlab"
githubIdentifier = "github"
giteaIdentifier = "gitea"
stashIdentifier = "stash"
bitbucketIdentifier = "bitbucket"
azuredevopsIdentifier = "azuredevops"
gitlabIdentifier = "gitlab"
githubIdentifier = "github"
giteaIdentifier = "gitea"
Comment thread
olblak marked this conversation as resolved.
stashIdentifier = "stash"
bitbucketIdentifier = "bitbucket"
)

// ErrWrongConfig is returned when an action has missing mandatory attributes.
Expand Down Expand Up @@ -148,6 +151,25 @@ func (a *Action) Update() error {
func (a *Action) generateActionHandler() error {
// Don't forget to update the JSONSchema() method when adding/updating/removing a case
switch a.Config.Kind {
case "azuredevops/pullrequest":
if a.Scm.Config.Kind != azuredevopsIdentifier {
return fmt.Errorf("scm of kind %q is not compatible with action of kind %q",
a.Scm.Config.Kind,
a.Config.Kind)
}

ge, ok := a.Scm.Handler.(*azuredevopsscm.AzureDevOps)
if !ok {
return fmt.Errorf("scm is not of kind 'azuredevops'")
}

g, err := azuredevops.New(a.Config.Spec, ge)
if err != nil {
return err
}

a.Handler = &g

Comment thread
olblak marked this conversation as resolved.
case "bitbucket/pullrequest", bitbucketIdentifier:
if a.Scm.Config.Kind != bitbucketIdentifier {
return fmt.Errorf("scm of kind %q is not compatible with action of kind %q",
Expand Down Expand Up @@ -267,11 +289,12 @@ func (Config) JSONSchema() *jschema.Schema {
type configAlias Config

anyOfSpec := map[string]interface{}{
"github/pullrequest": &github.ActionSpec{},
"gitea/pullrequest": &gitea.Spec{},
"stash/pullrequest": &stash.Spec{},
"gitlab/mergerequest": &gitlab.Spec{},
"bitbucket/pullrequest": &bitbucket.Spec{},
"azuredevops/pullrequest": &azuredevops.Spec{},
"github/pullrequest": &github.ActionSpec{},
"gitea/pullrequest": &gitea.Spec{},
"stash/pullrequest": &stash.Spec{},
"gitlab/mergerequest": &gitlab.Spec{},
"bitbucket/pullrequest": &bitbucket.Spec{},
}

return jsonschema.AppendOneOfToJsonSchema(configAlias{}, anyOfSpec)
Expand Down
11 changes: 11 additions & 0 deletions pkg/core/pipeline/action/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ func Test_Validate(t *testing.T) {
ScmID: "default",
},
},
{
name: "Passing case with 'azuredevops/pullrequest'",
config: Config{
Kind: "azuredevops/pullrequest",
ScmID: "default",
},
wantConfig: Config{
Kind: "azuredevops/pullrequest",
ScmID: "default",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
Loading