Question
- The TeamCity server has a large number of active build configurations with the configured Pull Requests build feature and automatic triggers that start builds on pull request branches.
- The Pull Requests build feature was initially configured to build draft pull requests.
- The new requirement is to disable the builds for all draft pull requests.
Is it possible to disable starting builds on draft pull requests for the whole server?
Answer
There are two ways to approach it:
- The pull requests are handled by the Pull Request build feature. When using GitHub or GitLab, the Ignore Drafts option can be enabled in the build feature settings: GitHub Pull Requests GitLab Merge Requests TeamCity will then completely ignore the drafts until their status changes to non-draft. For Azure DevOps, the drafts are always ignored.
- The Pull Requests build feature doesn't trigger builds on its own; it only loads the information, and builds are triggered by triggers. Most commonly, the VCS Trigger.
Triggers can be set up to trigger builds only on specific branches using the branch filter.
The branch filters on triggers can be used to filter pull requests.
For example, if the
+pr:draft=falseis added to the branch filter, it will include non-draft pull requests to the branches that will trigger the build. Or,-pr:draft=truecan be used to exclude the drafts. With such a setup, the drafts’ information will be loaded from the VCS to TeamCity by the Pull Request build feature, but the builds on them won't be triggered.
Both approaches can be automated using the REST API.
Automation example
The script that automates the second approach using the REST API should follow these steps:
- Get the list of build configurations on the server.
- Iterate through the build configurations and get the triggers.
- Modify the
branchFilterproperty of a trigger (add-pr:draft=trueto it), and usePUT /app/rest/buildTypes/id:{bt_id}/triggers/{trig_id}request to update the settings on the server.
Python implementation
#!/usr/bin/env python3
import requests
TC_URL = "https://teamcity.example.com" # no trailing slash
TC_TOKEN = "YOUR_PAT_HERE" # personal access token
SESSION = requests.Session()
SESSION.headers.update({
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": f"Bearer {TC_TOKEN}",
})
def tc_get(path, params=None):
r = SESSION.get(TC_URL + path, params=params)
r.raise_for_status()
return r.json()
def tc_put(path, json_body):
r = SESSION.put(TC_URL + path, json=json_body)
r.raise_for_status()
return r.json() if r.text else None
def update_branch_filter(filter_text: str | None) -> str | None:
"""
Given the current branchFilter, return a modified version that excludes
draft PRs via '-pr:draft=true', or None if no change is needed.
"""
if filter_text is None:
lines = []
else:
lines = [l for l in filter_text.splitlines() if l.strip() != ""]
# If there's already an explicit draft= condition, assume the admin
# configured it intentionally and skip.
if any("draft=" in l for l in lines):
return None
# Append rule that excludes only draft PRs; non-draft PRs keep existing behavior.
lines.append("-pr:draft=true")
return "\n".join(lines)
def main():
# Get all build configurations (id + name only)
bt_resp = tc_get(
"/app/rest/buildTypes",
params={"fields": "buildType(id,name,href)"}
)
build_types = bt_resp.get("buildType", [])
print(f"Found {len(build_types)} build configurations")
for bt in build_types:
bt_id = bt["id"]
bt_name = bt.get("name", bt_id)
try:
# Get triggers for this build configuration
trig_resp = tc_get(
f"/app/rest/buildTypes/id:{bt_id}/triggers",
params={
"fields": "trigger(id,type,disabled,properties(property(name,value)))"
}
)
except requests.HTTPError as e:
# No triggers or insufficient permissions, skip
print(f"[{bt_id}] {bt_name}: failed to get triggers: {e}")
continue
triggers = trig_resp.get("trigger", [])
if not triggers:
continue
for trig in triggers:
if trig.get("type") != "vcsTrigger":
continue
trig_id = trig["id"]
props = trig.get("properties", {}).get("property", [])
# Find existing branchFilter property (if any)
bf_prop = next((p for p in props if p.get("name") == "branchFilter"), None)
current_bf = bf_prop.get("value") if bf_prop else None
new_bf = update_branch_filter(current_bf)
if new_bf is None or new_bf == current_bf:
continue # nothing to change
if bf_prop:
bf_prop["value"] = new_bf
else:
props.append({"name": "branchFilter", "value": new_bf})
trig.setdefault("properties", {})["property"] = props
# PUT updated trigger back
try:
tc_put(
f"/app/rest/buildTypes/id:{bt_id}/triggers/{trig_id}",
json_body=trig
)
print(f"[{bt_id}] {bt_name}: updated VCS trigger {trig_id}")
except requests.HTTPError as e:
print(f"[{bt_id}] {bt_name}: FAILED to update trigger {trig_id}: {e}")
if __name__ == "__main__":
main()
Comments
0 comments
Please sign in to leave a comment.