{"id":3903,"date":"2026-04-22T18:20:18","date_gmt":"2026-04-22T18:20:18","guid":{"rendered":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/04\/22\/optimizing-git-policy-management-at-scale\/"},"modified":"2026-04-22T18:20:18","modified_gmt":"2026-04-22T18:20:18","slug":"optimizing-git-policy-management-at-scale","status":"publish","type":"post","link":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/04\/22\/optimizing-git-policy-management-at-scale\/","title":{"rendered":"Optimizing Git policy management at scale"},"content":{"rendered":"<p>With just a single improvement in the REST API of Azure DevOps, we achieved a massive reduction in CPU usage and execution time when managing Git policies: 2x less CPU and 10-15x faster execution!<\/p>\n<p>This change is already available to all users of <a href=\"https:\/\/azure.com\/devops\">Azure DevOps<\/a>, and it\u2019s time to share a bit more detail: the background, what the change is, and how it helped us improve the performance.<\/p>\n<p>You may find this article useful if you maintain automation that manages Git policy configurations in <a href=\"https:\/\/azure.microsoft.com\/en-us\/products\/devops\/repos\">Azure Repos<\/a> using REST API.<\/p>\n<h2>Git policy governance at a big enterprise<\/h2>\n<p>Git policies are crucial for maintaining high quality of code and preventing malicious changes. They define rules enforced before code can land in repos and protected branches.<\/p>\n<p>Azure Repos has a rich policy engine that lets you configure things like a minimum number of reviewers, specific reviewers who must sign off when certain parts of the code are updated, checking for credentials and secrets on push, and much more.<\/p>\n<p>When the number of products, services and repos outgrows a certain point, ensuring the right policies are configured becomes a challenge. Human errors are unavoidable: it\u2019s very easy to misconfigure something, for example, the service\u2019s behavior when follow-up changes are pushed to an already approved pull request. Even if the initial state across the repos is correct, configuration often drifts over time when managed manually.<\/p>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2026\/04\/Pasted-image-20260418182608.webp\"><img data-opt-id=1829861929  fetchpriority=\"high\" decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2026\/04\/Pasted-image-20260418182608-300x216.webp\" alt='Screenshot from Azure Repos UI showing \"Require a minimum number of reviewers\" policy that is configured to require at least one approval on the last iteration when new changes are pushed' width=\"300\" height=\"216\" class=\"alignnone size-medium wp-image-72701\" \/><\/a><\/p>\n<p>Microsoft offers a wide range of products. Our own code is exclusively hosted in Azure Repos and GitHub, across hundreds of thousands of Git repos, ranging from tiny polyrepos to the largest Git monorepos in the world.<\/p>\n<p>For these reasons, we and many other big enterprises rely on the REST API in Azure DevOps and GitHub to audit and mitigate policy misconfiguration and drift across the entire portfolio of repos. A dedicated service defines the target state of policies and automatically updates them when the actual state drifts from the target.<\/p>\n<p>Let me walk you through how the policies are stored and retrieved.<\/p>\n<h2>How policies relate to projects, repos and branches<\/h2>\n<p>There are two types of Git policies in Azure Repos: push and branch policies. They can also be called repository and pull request policies.<\/p>\n<p>Push policies define the rules of what can enter repos and what cannot, no matter the branch. For example, if someone is trying to push new commits containing any secrets, such a push is rejected, even when pushed to a user branch.<\/p>\n<p>Branch policies, on the other hand, protect specific branches (like <code>main<\/code>) and require all changes to go via pull requests. For example, they can enforce that code builds and all the tests are green before letting a change land on a protected branch.<\/p>\n<p>In short, different types of policies affect either entire repos or branches inside them.<\/p>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2026\/04\/Drawing-2026-04-18-18.30.31.excalidraw.webp\"><img data-opt-id=1044579750  fetchpriority=\"high\" decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2026\/04\/Drawing-2026-04-18-18.30.31.excalidraw-300x147.webp\" alt=\"Visualization showing that ADO organization contains projects, and project contains git repos. &quot;Require a minimum number of reviewers&quot; policy affects individual branches like main and releases\/v1 while Advanced Security's &quot;Block secrets on push&quot; policy affects entire git repos.\" width=\"300\" height=\"147\" class=\"alignnone size-medium wp-image-72702\" \/><\/a><\/p>\n<p>The policy engine also supports different scopes and inheritance. You can define a policy for a specific branch <code>main<\/code> in a specific repository, or you can configure a policy that will affect all branches matching <code>releases\/*<\/code> in all repos in a project. The support of cross-repo policies and glob patterns partially solves the maintenance burden described earlier, but is not enough, especially when the repos are spread across many different projects and Azure DevOps organizations.<\/p>\n<p>Two kinds of policies and support of inheritance define the architecture of how the policies are stored.<\/p>\n<p>Each project has its own logical container for the policies, instead of each repo or branch having its own container. This helps to power cross-repo policies as well as support branch glob patterns like <code>releases\/*<\/code>.<\/p>\n<p>With that, all the policy configurations are linked to a specific project, but not a repo or branch. Then how does the engine know which ones to check for a specific operation, like a push or PR completion attempt? This is where the <code>Scope<\/code> field comes into play.<\/p>\n<h2>Policy scope<\/h2>\n<p>This is a simple string that specifies where in the inheritance hierarchy a given policy is <em>defined<\/em>. It contains one or two parts separated by a colon:<\/p>\n<ul>\n<li>Repo ID \u2013 which repo the policy applies to;<\/li>\n<li>Ref ID (optional) \u2013 which ref (branch) the policy applies to.<\/li>\n<\/ul>\n<p>An example of a <code>Scope<\/code> value for a branch policy:<\/p>\n<pre><code>2c938d1f6e6f458d816484fc51e7cf74:refs\/heads\/main\n<\/code><\/pre>\n<p>Such a value makes the branch policy apply only to the <code>main<\/code> branch in the repo with ID <code>2c938d1f-6e6f-458d-8164-84fc51e7cf74<\/code>.<\/p>\n<p>A couple of edge cases:<\/p>\n<ul>\n<li><code>2c938d1f6e6f458d816484fc51e7cf74<\/code> \u2013 a push policy for the repo with that ID \u2013 doesn\u2019t contain the ref part.<\/li>\n<li><code>*:refs\/heads\/releases\/*<\/code> \u2013 cross-repo branch policy that applies to the branches matching <code>releases\/v1<\/code>, <code>releases\/v2<\/code>, etc.<\/li>\n<\/ul>\n<p>The last piece of the puzzle: the available endpoints to fetch the policies.<\/p>\n<h2>Querying the policies with REST API<\/h2>\n<p>Azure DevOps offers two REST API endpoints to list policy configurations:<\/p>\n<p>The <a href=\"https:\/\/learn.microsoft.com\/en-us\/rest\/api\/azure\/devops\/policy\/configurations\/list?view=azure-devops-rest-7.2&amp;tabs=HTTP\"><code>GET \/_apis\/policy\/configurations<\/code><\/a> is an older endpoint that allows querying all the policies in a project, with optional scope filtering <strong>without support of inheritance<\/strong>. If you pass <code>2c938d1f6e6f458d816484fc51e7cf74:refs\/heads\/releases\/v1<\/code>, it will only return the policies with that exact value of the scope, but it won\u2019t return a policy with the scope of <code>*:refs\/heads\/releases\/*<\/code>, which also applies to that branch.<\/p>\n<p>The other endpoint is <a href=\"https:\/\/learn.microsoft.com\/en-us\/rest\/api\/azure\/devops\/git\/policy-configurations\/get?view=azure-devops-rest-7.2\"><code>GET \/_apis\/git\/policy\/configurations<\/code><\/a> (mind the <code>\/git\/<\/code>). Instead of the single exact value of <code>scope<\/code>, it accepts the <code>repositoryId<\/code> \/ <code>refName<\/code> pair.<\/p>\n<p>If the first endpoint is useful to fetch which policies are <em>defined<\/em> at a specific scope, the second one is useful to fetch which <em>apply<\/em> to a specific scope by using inheritance-aware filtering.<\/p>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2026\/04\/Drawing-2026-04-20-08.40.11.excalidraw.webp\"><img data-opt-id=1857135965  data-opt-src=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2026\/04\/Drawing-2026-04-20-08.40.11.excalidraw-300x128.webp\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20300%20128%22%20width%3D%22300%22%20height%3D%22128%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22300%22%20height%3D%22128%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt='Visualization showing git policy inheritance hierarchy: policies at Project scope are inherited by repos, branch folders like releases\/* and individual branches like releases\/v1. The \"GET \/_apis\/git\/policy\/configurations\" endpoint returns all policies including inherited while the \"GET \/_apis\/policy\/configurations\" endpoint only returns policies defined at requested scope without inherited policies.' width=\"300\" height=\"128\" class=\"alignnone size-medium wp-image-72703\" \/><\/a><\/p>\n<p>If you pass <code>repositoryId=2c938d1f-6e6f-458d-8164-84fc51e7cf74<\/code> and <code>refName=refs\/heads\/releases\/v1<\/code>, it will return all the policies defined at the following scopes:<\/p>\n<ul>\n<li><code>2c938d1f6e6f458d816484fc51e7cf74<\/code><\/li>\n<li><code>*<\/code><\/li>\n<li><code>2c938d1f6e6f458d816484fc51e7cf74:refs\/heads\/releases\/v1<\/code><\/li>\n<li><code>*:refs\/heads\/releases\/v1<\/code><\/li>\n<li><code>2c938d1f6e6f458d816484fc51e7cf74:refs\/heads\/releases\/*<\/code><\/li>\n<li><code>*:refs\/heads\/releases\/*<\/code><\/li>\n<li><code>2c938d1f6e6f458d816484fc51e7cf74:refs\/heads\/*<\/code><\/li>\n<li><code>*:refs\/heads\/*<\/code><\/li>\n<li><code>2c938d1f6e6f458d816484fc51e7cf74:refs\/*<\/code><\/li>\n<li><code>*:refs\/*<\/code><\/li>\n<\/ul>\n<p>Thanks to filtering by multiple scope values dynamically, it helps to answer the question \u201cWhat protects my <code>releases\/v1<\/code> branch in repo X?\u201d, instead of \u201cWhich policies are defined at the level of <code>releases\/v1<\/code> branch in repo X?\u201d. It takes into account both the policies defined at that exact scope and any policies inherited from parent scopes.<\/p>\n<p>With that context, here\u2019s the problem with policy management at scale.<\/p>\n<h2>The Problem: Querying all the policies for one repo<\/h2>\n<p>The enterprise policy management service described earlier has one repo as a unit of work. It takes one specific repo, computes the target state of policies using predefined rules, retrieves the effective state currently configured in Azure DevOps, and then issues a set of policy create\/update\/delete requests if the effective state is misaligned with the target state. The service needs to see every single policy that applies to the repo as a whole, as well as any branch in that repo.<\/p>\n<p>The only problem is\u2026 There was no way to ask the REST API \u201cGive me all the policies that apply to a given repo and any of its branches\u201d. At least, not until now.<\/p>\n<p>As previously mentioned, the first endpoint (<a href=\"https:\/\/learn.microsoft.com\/en-us\/rest\/api\/azure\/devops\/policy\/configurations\/list?view=azure-devops-rest-7.2&amp;tabs=HTTP\"><code>GET \/_apis\/policy\/configurations<\/code><\/a>) can only filter by the exact value of the scope, but many possible scope values can match a repo, and you can\u2019t predict them all. You can pass <code>repositoryId<\/code> without specifying <code>refName<\/code> to the second endpoint (<a href=\"https:\/\/learn.microsoft.com\/en-us\/rest\/api\/azure\/devops\/git\/policy-configurations\/get?view=azure-devops-rest-7.2\"><code>GET \/_apis\/git\/policy\/configurations<\/code><\/a>), but you will only see the policies that apply to the repository as a whole: direct and inherited push policies. Branch policies aren\u2019t included because they don\u2019t affect the whole repository.<\/p>\n<p>How did our service work before the change? The only option was to query all the policies for the entire project using the first endpoint, and perform client-side filtering. This is not critical for most projects, but becomes a big challenge for the ones with thousands of repos and hundreds of thousands of policies. The service ended up serializing and returning hundreds of megabytes of data for every request, and the client had to deserialize and process it. This implied massive overhead, much longer execution time, and more CPU cycles spent by the server and the client.<\/p>\n<h2>The Solution<\/h2>\n<p>The second inheritance-aware endpoint (<a href=\"https:\/\/learn.microsoft.com\/en-us\/rest\/api\/azure\/devops\/git\/policy-configurations\/get?view=azure-devops-rest-7.2\"><code>GET \/_apis\/git\/policy\/configurations<\/code><\/a>) now supports a special value <code>~all<\/code> for the <code>refName<\/code> parameter. When passed together with <code>repositoryId<\/code>, it returns every single branch policy affecting any branch in the repo, as well as any push policy affecting the entire repo, inherited and defined at the scopes within the repo.<\/p>\n<p>Internally, the service takes all the policies in a project and only keeps the ones with scope starting with <code>*<\/code> (project-level policies affecting all the repos) or with <code>2c938d1f6e6f458d816484fc51e7cf74<\/code>. This includes both push and branch policies.<\/p>\n<p>After switching the policy management client to using <code>refName=~all<\/code> instead of doing client-side filtering, the overall server-side CPU consumption dropped by half, across all the endpoints for this client.<\/p>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2026\/04\/Pasted-image-20260413004123.webp\"><img data-opt-id=46609135  data-opt-src=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2026\/04\/Pasted-image-20260413004123-300x167.webp\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20300%20167%22%20width%3D%22300%22%20height%3D%22167%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22300%22%20height%3D%22167%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"Time chart showing overall CPU utilization over time with clear stabilized drop.\" width=\"300\" height=\"167\" class=\"alignnone size-medium wp-image-72699\" \/><\/a><\/p>\n<p>The drop in total wall-clock time across all requests is even more impressive \u2013 from 1-3 thousand hours per day down to only ~100-150, more than 10x-15x improvement!<\/p>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2026\/04\/Pasted-image-20260413011224.webp\"><img data-opt-id=156486108  data-opt-src=\"https:\/\/devblogs.microsoft.com\/devops\/wp-content\/uploads\/sites\/6\/2026\/04\/Pasted-image-20260413011224-300x153.webp\"  decoding=\"async\" src=\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20300%20153%22%20width%3D%22300%22%20height%3D%22153%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22300%22%20height%3D%22153%22%20fill%3D%22transparent%22%2F%3E%3C%2Fsvg%3E\" alt=\"Time chart showing overall wall-clock execution time with clear stabilized drop.\" width=\"300\" height=\"153\" class=\"alignnone size-medium wp-image-72700\" \/><\/a><\/p>\n<h2>Conclusion<\/h2>\n<p>For enterprises with large engineering teams, automated policy governance becomes a necessity. The new <code>refName=~all<\/code> feature further simplifies this process and can significantly improve the performance of your automation. Make use of our REST API to achieve that:<\/p>\n<ul>\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/rest\/api\/azure\/devops\/policy\/?view=azure-devops-rest-7.2\"><code>\/_apis\/policy\/<\/code> set of endpoints<\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/rest\/api\/azure\/devops\/git\/policy-configurations?view=azure-devops-rest-7.2\"><code>\/_apis\/git\/policy\/configurations<\/code> endpoint<\/a><\/li>\n<\/ul>\n<p>For more information about Git policies, see these docs on Microsoft Learn:<\/p>\n<ul>\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/devops\/repos\/git\/repository-settings?view=azure-devops&amp;tabs=browser\">Set Git Repository Settings<\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/devops\/repos\/git\/branch-policies?view=azure-devops&amp;tabs=browser\">Git branch policies and settings<\/a><\/li>\n<\/ul>\n<p>Let me know in the comments below if your team faces similar challenges. I\u2019ll be happy to chat and answer your questions.<\/p>\n<p>The post <a href=\"https:\/\/devblogs.microsoft.com\/devops\/optimizing-git-policy-management-at-scale\/\">Optimizing Git policy management at scale<\/a> appeared first on <a href=\"https:\/\/devblogs.microsoft.com\/devops\">Azure DevOps Blog<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>With just a single improvement in the REST API of Azure DevOps, we achieved a massive reduction in CPU usage [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":3904,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[3],"tags":[],"class_list":["post-3903","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-azure"],"_links":{"self":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/3903","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/comments?post=3903"}],"version-history":[{"count":0,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/3903\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media\/3904"}],"wp:attachment":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media?parent=3903"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/categories?post=3903"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/tags?post=3903"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}