Skip to content

Commit a9bde00

Browse files
cheenamalhotraCopilotCopilot
authored
Setup autosync workflow (dotnet#4221)
* Setup autosync workflow Co-authored-by: Copilot <copilot@github.com> * Update comment * Updates Co-authored-by: Copilot <copilot@github.com> * Update pipeline definition * Fix error when only 1 commit exists * throw with error * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 7a896ef commit a9bde00

2 files changed

Lines changed: 380 additions & 0 deletions

File tree

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#################################################################################
2+
# Licensed to the .NET Foundation under one or more agreements. #
3+
# The .NET Foundation licenses this file to you under the MIT license. #
4+
# See the LICENSE file in the project root for more information. #
5+
#################################################################################
6+
7+
# This pipeline synchronizes the GitHub dotnet/SqlClient repository's main
8+
# branch into the internal ADO repository by:
9+
#
10+
# 1. Fetching the latest commits from GitHub main.
11+
# 2. Pushing them to a dev/autosync/github-main branch in ADO.
12+
# 3. Creating (or updating) a pull request targeting internal/main.
13+
#
14+
# It runs on a daily schedule at 02:00 UTC and can also be triggered manually.
15+
# The pipeline does not auto-complete the PR — changes must be reviewed and
16+
# merged manually.
17+
#
18+
# Prerequisites:
19+
#
20+
# - The build service identity must have "Contribute", "Create branch", and
21+
# "Contribute to pull requests" permissions on the ADO repository.
22+
#
23+
# This pipeline definition is mapped to the following Azure DevOps pipeline:
24+
#
25+
# - GitHub Sync in the ADO.Net project:
26+
#
27+
# https://sqlclientdrivers.visualstudio.com/ADO.Net/_build?definitionId=2263
28+
29+
# Set the pipeline run name to the calendar date (yyyyMMdd) and the daily run counter.
30+
name: Sync-$(Date:yyyyMMdd)$(Rev:.r)
31+
32+
# Do not trigger this pipeline on commits or PRs.
33+
trigger: none
34+
pr: none
35+
36+
# Trigger this pipeline on a daily schedule.
37+
schedules:
38+
- cron: '0 2 * * *'
39+
displayName: Daily GitHub Sync (02:00 UTC)
40+
branches:
41+
include:
42+
- internal/main
43+
always: true
44+
45+
# Pipeline parameters, visible in the Azure DevOps UI.
46+
parameters:
47+
48+
# The GitHub branch to sync from.
49+
- name: githubBranch
50+
displayName: GitHub Branch
51+
type: string
52+
default: main
53+
54+
# The ADO target branch to create the PR against.
55+
- name: targetBranch
56+
displayName: ADO Target Branch
57+
type: string
58+
default: internal/main
59+
60+
jobs:
61+
- job: SyncGitHub
62+
displayName: Sync GitHub to ADO
63+
pool:
64+
vmImage: 'ubuntu-latest'
65+
66+
steps:
67+
# Check out the ADO repo with full history so we can compare branches.
68+
- checkout: self
69+
persistCredentials: true
70+
fetchDepth: 0
71+
72+
# Run the sync script.
73+
- task: PowerShell@2
74+
displayName: Sync GitHub main to ADO
75+
inputs:
76+
filePath: $(Build.SourcesDirectory)/eng/pipelines/scripts/Sync-GitHubToAdo.ps1
77+
arguments: >-
78+
-GitHubRepoUrl "https://github.com/dotnet/SqlClient.git"
79+
-GitHubBranch "${{ parameters.githubBranch }}"
80+
-TargetBranch "${{ parameters.targetBranch }}"
81+
-SyncBranchName "dev/autosync/github-${{ parameters.githubBranch }}"
82+
-AdoOrgUrl "$(System.CollectionUri)"
83+
-AdoProject "$(System.TeamProject)"
84+
-AdoRepoName "$(Build.Repository.Name)"
85+
pwsh: true
86+
env:
87+
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
<#
2+
.SYNOPSIS
3+
Syncs a GitHub branch into an Azure DevOps repository via a pull request.
4+
5+
.DESCRIPTION
6+
This script fetches the latest commits from a public GitHub repository,
7+
force-pushes them to a sync branch in the ADO repo, then creates or updates
8+
a pull request targeting the specified ADO branch.
9+
10+
If there are no new commits (the branches are already in sync), the script
11+
exits cleanly with no changes.
12+
13+
.PARAMETER GitHubRepoUrl
14+
The HTTPS clone URL of the public GitHub repository.
15+
16+
.PARAMETER GitHubBranch
17+
The branch to sync from GitHub (e.g. "main").
18+
19+
.PARAMETER TargetBranch
20+
The ADO branch the PR should target (e.g. "internal/main").
21+
22+
.PARAMETER SyncBranchName
23+
The ADO branch name to push GitHub commits to (e.g. "dev/autosync/github-main").
24+
25+
.PARAMETER AdoOrgUrl
26+
The Azure DevOps organization URL (e.g. "https://dev.azure.com/org/").
27+
28+
.PARAMETER AdoProject
29+
The Azure DevOps project name.
30+
31+
.PARAMETER AdoRepoName
32+
The Azure DevOps repository name.
33+
34+
.PARAMETER AccessToken
35+
The access token for ADO REST API and git push operations.
36+
Defaults to the SYSTEM_ACCESSTOKEN environment variable.
37+
38+
.NOTES
39+
This pipeline is intended to be run only in the internal ADO.Net project.
40+
It must never be run in the Public project or triggered by changes in GitHub.
41+
#>
42+
43+
# Licensed to the .NET Foundation under one or more agreements.
44+
# The .NET Foundation licenses this file to you under the MIT license.
45+
# See the LICENSE file in the project root for more information.
46+
47+
[CmdletBinding()]
48+
param(
49+
[Parameter(Mandatory)]
50+
[string]$GitHubRepoUrl,
51+
52+
[Parameter(Mandatory)]
53+
[string]$GitHubBranch,
54+
55+
[Parameter(Mandatory)]
56+
[string]$TargetBranch,
57+
58+
[Parameter(Mandatory)]
59+
[string]$SyncBranchName,
60+
61+
[Parameter(Mandatory)]
62+
[string]$AdoOrgUrl,
63+
64+
[Parameter(Mandatory)]
65+
[string]$AdoProject,
66+
67+
[Parameter(Mandatory)]
68+
[string]$AdoRepoName,
69+
70+
[string]$AccessToken = $env:SYSTEM_ACCESSTOKEN
71+
)
72+
73+
Set-StrictMode -Version Latest
74+
$ErrorActionPreference = 'Stop'
75+
76+
#region Validation
77+
if ([string]::IsNullOrWhiteSpace($AccessToken)) {
78+
throw "Access token is required. Set SYSTEM_ACCESSTOKEN or pass -AccessToken."
79+
}
80+
#endregion
81+
82+
#region Helper Functions
83+
84+
function Invoke-AdoApi {
85+
<#
86+
.SYNOPSIS
87+
Calls an Azure DevOps REST API endpoint.
88+
#>
89+
param(
90+
[Parameter(Mandatory)][string]$Uri,
91+
[string]$Method = 'GET',
92+
[object]$Body = $null
93+
)
94+
95+
$headers = @{
96+
'Authorization' = "Bearer $AccessToken"
97+
'Content-Type' = 'application/json'
98+
}
99+
100+
$params = @{
101+
Uri = $Uri
102+
Method = $Method
103+
Headers = $headers
104+
}
105+
106+
if ($null -ne $Body) {
107+
$params['Body'] = ($Body | ConvertTo-Json -Depth 10)
108+
}
109+
110+
$response = Invoke-RestMethod @params -ErrorAction Stop
111+
return $response
112+
}
113+
114+
function Get-CommitSummary {
115+
<#
116+
.SYNOPSIS
117+
Returns a markdown-formatted list of commits between two refs.
118+
#>
119+
param(
120+
[Parameter(Mandatory)][string]$BaseRef,
121+
[Parameter(Mandatory)][string]$HeadRef,
122+
[int]$MaxCommits = 50
123+
)
124+
125+
$logOutput = git log --oneline "$BaseRef..$HeadRef" -n $MaxCommits 2>&1
126+
if ($LASTEXITCODE -ne 0) {
127+
Write-Warning "Could not retrieve commit log: $logOutput"
128+
return "_(commit log unavailable)_"
129+
}
130+
131+
$lines = @($logOutput -split "`n" | Where-Object { $_ -match '\S' })
132+
if ($lines.Count -eq 0) {
133+
return "_(no commits)_"
134+
}
135+
136+
$summary = ($lines | ForEach-Object { "- ``$_``" }) -join "`n"
137+
138+
$totalCount = (git rev-list --count "$BaseRef..$HeadRef" 2>&1)
139+
if ($LASTEXITCODE -eq 0 -and [int]$totalCount -gt $MaxCommits) {
140+
$summary += "`n`n_...and $($totalCount - $MaxCommits) more commit(s)._"
141+
}
142+
143+
return $summary
144+
}
145+
146+
#endregion
147+
148+
#region Git Operations
149+
150+
Write-Host "=== GitHub to ADO Sync ==="
151+
Write-Host "GitHub : $GitHubRepoUrl @ $GitHubBranch"
152+
Write-Host "ADO : $AdoProject/$AdoRepoName @ $TargetBranch"
153+
Write-Host "Sync : $SyncBranchName"
154+
Write-Host ""
155+
156+
# Configure git identity for any merge commits (shouldn't be needed for
157+
# force-push, but set it defensively).
158+
git config user.email "ado-sync-bot@microsoft.com"
159+
git config user.name "ADO Sync Bot"
160+
161+
# Add GitHub as a remote and fetch the branch we want to sync.
162+
Write-Host "Fetching GitHub branch '$GitHubBranch'..."
163+
$remoteExists = git remote | Where-Object { $_ -eq 'github' }
164+
if ($remoteExists) {
165+
git remote set-url github $GitHubRepoUrl
166+
} else {
167+
git remote add github $GitHubRepoUrl
168+
}
169+
170+
git fetch github $GitHubBranch --verbose
171+
if ($LASTEXITCODE -ne 0) {
172+
throw "Failed to fetch '$GitHubBranch' from GitHub."
173+
}
174+
175+
# Resolve the SHA of the fetched GitHub branch.
176+
$githubSha = git rev-parse "github/$GitHubBranch"
177+
if ($LASTEXITCODE -ne 0) {
178+
throw "Failed to resolve SHA for 'github/$GitHubBranch'."
179+
}
180+
Write-Host "GitHub HEAD : $githubSha"
181+
182+
# Check if the target branch exists and compare SHAs.
183+
$targetRef = git rev-parse "origin/$TargetBranch" 2>&1
184+
$targetExists = ($LASTEXITCODE -eq 0)
185+
186+
if ($targetExists) {
187+
Write-Host "Target HEAD : $targetRef"
188+
}
189+
190+
# Check if the sync branch already exists in origin.
191+
$syncRef = git rev-parse "origin/$SyncBranchName" 2>&1
192+
$syncBranchExists = ($LASTEXITCODE -eq 0)
193+
194+
if ($syncBranchExists -and $syncRef -eq $githubSha) {
195+
Write-Host ""
196+
Write-Host "Sync branch is already at GitHub HEAD. Nothing to do."
197+
exit 0
198+
}
199+
200+
# Create the sync branch pointing to the GitHub HEAD and force-push it.
201+
Write-Host ""
202+
Write-Host "Updating sync branch '$SyncBranchName' to GitHub HEAD..."
203+
git checkout -B $SyncBranchName "github/$GitHubBranch" --quiet
204+
if ($LASTEXITCODE -ne 0) {
205+
throw "Failed to create sync branch."
206+
}
207+
208+
git push origin $SyncBranchName --force --quiet
209+
if ($LASTEXITCODE -ne 0) {
210+
throw "Failed to push sync branch to ADO."
211+
}
212+
Write-Host "Sync branch pushed successfully."
213+
214+
#endregion
215+
216+
#region Pull Request Management
217+
218+
# Build the commit summary for the PR description / comment.
219+
$commitSummary = if ($targetExists) {
220+
Get-CommitSummary -BaseRef "origin/$TargetBranch" -HeadRef "github/$GitHubBranch"
221+
} else {
222+
"_(target branch does not exist yet — initial sync)_"
223+
}
224+
225+
# Normalize the ADO org URL (remove trailing slash for consistent URI construction).
226+
$AdoOrgUrl = $AdoOrgUrl.TrimEnd('/')
227+
228+
# URL-encode the project name for REST API calls.
229+
$encodedProject = [Uri]::EscapeDataString($AdoProject)
230+
$encodedRepo = [Uri]::EscapeDataString($AdoRepoName)
231+
$apiBase = "$AdoOrgUrl/$encodedProject/_apis/git/repositories/$encodedRepo"
232+
233+
# Search for an existing active PR from the sync branch to the target branch.
234+
Write-Host ""
235+
Write-Host "Checking for existing pull requests..."
236+
237+
$encodedSyncBranch = [Uri]::EscapeDataString($SyncBranchName)
238+
$encodedTargetBranch = [Uri]::EscapeDataString($TargetBranch)
239+
$searchUri = "$apiBase/pullrequests?searchCriteria.sourceRefName=refs/heads/$encodedSyncBranch" +
240+
"&searchCriteria.targetRefName=refs/heads/$encodedTargetBranch" +
241+
"&searchCriteria.status=active" +
242+
"&api-version=7.1"
243+
244+
$existingPrs = Invoke-AdoApi -Uri $searchUri
245+
$activePr = $existingPrs.value | Select-Object -First 1
246+
247+
if ($activePr) {
248+
# An active PR already exists — update its description and add a comment.
249+
$prId = $activePr.pullRequestId
250+
Write-Host "Active PR #$prId found. Updating description and posting comment..."
251+
252+
# 1. PATCH the PR description with the refreshed commit summary.
253+
$patchUri = "$apiBase/pullrequests/$prId?api-version=7.1"
254+
$patchBody = @{
255+
description = "## Automated GitHub Sync`n`nThis PR was updated automatically by the GitHub sync pipeline.`n`nSource: [dotnet/SqlClient@$GitHubBranch](https://github.com/dotnet/SqlClient/tree/$GitHubBranch)`nSync branch: ``$SyncBranchName```n`n### Latest Commits`n`n$commitSummary`n`n---`n_This PR requires manual review and merge. Auto-complete is not enabled._"
256+
}
257+
Invoke-AdoApi -Uri $patchUri -Method 'PATCH' -Body $patchBody | Out-Null
258+
Write-Host "PR #$prId description updated."
259+
260+
# 2. Post a comment thread summarising the new commits.
261+
$commentUri = "$apiBase/pullrequests/$prId/threads?api-version=7.1"
262+
$commentBody = @{
263+
comments = @(
264+
@{
265+
parentCommentId = 0
266+
content = "## Sync Update`n`nPR updated by the GitHub sync pipeline on $((Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm')) UTC.`n`nNew commits from GitHub ``$GitHubBranch``:`n`n$commitSummary"
267+
commentType = 1 # Text
268+
}
269+
)
270+
status = 1 # Active
271+
}
272+
Invoke-AdoApi -Uri $commentUri -Method 'POST' -Body $commentBody | Out-Null
273+
Write-Host "Update comment posted to PR #$prId."
274+
} else {
275+
# No active PR — create a new one.
276+
Write-Host "No active PR found. Creating a new pull request..."
277+
278+
$prUri = "$apiBase/pullrequests?api-version=7.1"
279+
$prBody = @{
280+
sourceRefName = "refs/heads/$SyncBranchName"
281+
targetRefName = "refs/heads/$TargetBranch"
282+
title = "[GitHub Sync] Update $TargetBranch from GitHub $GitHubBranch"
283+
description = "## Automated GitHub Sync`n`nThis PR was created automatically by the GitHub sync pipeline.`n`nSource: [dotnet/SqlClient@$GitHubBranch](https://github.com/dotnet/SqlClient/tree/$GitHubBranch)`nSync branch: ``$SyncBranchName```n`n### Commits`n`n$commitSummary`n`n---`n_This PR requires manual review and merge. Auto-complete is not enabled._"
284+
}
285+
286+
$newPr = Invoke-AdoApi -Uri $prUri -Method 'POST' -Body $prBody
287+
Write-Host "Created PR #$($newPr.pullRequestId): $($newPr.title)"
288+
}
289+
290+
#endregion
291+
292+
Write-Host ""
293+
Write-Host "=== Sync complete ==="

0 commit comments

Comments
 (0)