Skip to content

Instantly share code, notes, and snippets.

@mu88
Last active September 15, 2022 09:10
Show Gist options
  • Save mu88/d9fddd52e226ee1355f19eace55e2603 to your computer and use it in GitHub Desktop.
Save mu88/d9fddd52e226ee1355f19eace55e2603 to your computer and use it in GitHub Desktop.
Automatically upgrade NuGet packages and create PR in Bitbucket

Automatically upgrade NuGet packages and create PR in Bitbucket

Since NuKeeper is deprecated, I dediced to write it very basic PowerShell script which does the same for our on-prem Bitbucket instance. It uses the .NET global tool dotnet-outdated.

Prerequisites

Some things that come to my mind about this script:

  • Tested with PowerShell 7.2.5 and Bitbucket 7.21 (on-prem)
  • Script has to be executed within an already cloned Git repository
  • Git repository must use http or https for the remote
  • There must be a user inside Bitbucket with write permissions. The user needs an HTTP Access Token to communicate with Bitbucket.
  • dotnet-outdated seems to have some issues with the old CSPROJ format, so make sure to use it only with the newer SDK-style projects.

Approach

  1. Determine some basic Bitbucket parameters (name of Bitbucket host, project, repository, etc.) based on the Git repository's remote URL
  2. Check if there is an existing Bitbucket PR called Automatic update of NuGet packages (see parameter $pullRequestUpdateIdentifier) for the current repo. If yes, that one has to be finished and the script will be terminated.
  3. Run dotnet-outdated for all specified .NET solutions (see parameter $solutions). If you specify ALL_PROJECTS, a temporary solution with all available C# projects will be created and upgraded.
    Certain NuGet packages can be excluded or included using a comma-delimited list (see parameters $packagesToExclude and $packagesToInclude - you cannot use both parameters at the same time). Locking of package versions is also supported (see here and parameter $versionLock).
  4. If Git detects changes in the current directory, a new commit (see parameter $commitMessage) inside a new branch feature/<<some GUID>> (see parameter $newBranchWithUpdates) will be created and pushed.
  5. A new PR inside Bitbucket will be opened with the repo's default reviewers.
Clear-Host
$ErrorActionPreference = 'Stop'
# Public parameters (to be changed)
$bitbucketUser = "yourBitbucketUser"
$bitbucketToken = "yourBitbucketToken"
$solutions = "SolutionA.sln, SolutionB.sln"
$packagesToExclude = "Aspose, PdfTools"
$packagesToInclude = ""
$versionLock = "None" # use "None", "Major" or "Minor"
$commitMessage = "chore: automatically update NuGet packages"
$pushAndCreatePullRequest = $true
# Internal parameters (not to be changed)
$allProjectsIdentifier = "ALL_PROJECTS"
$pullRequestUpdateIdentifier = "Automatic update of NuGet packages"
$guid = New-Guid
$newBranchWithUpdates = "feature/${guid}"
$defaultBranch = git branch --show-current
$remoteOriginUrl = git config --get remote.origin.url
$remoteOriginUrl -match "(?<HostWithProtocol>(?<ProtocolWithSlash>(?<Protocol>http|https):\/\/)(?<Host>[^\/]*))\/scm\/(?<Project>[^\/]*)\/(?<Repository>[^\/]*).git"
$bitbucketHostWithProtocol = $Matches.HostWithProtocol
$bitbucketHost = $Matches.Host
$bitbucketProtocolWithSlash = $Matches.ProtocolWithSlash
$bitbucketProject = $Matches.Project
$bitbucketRepo = $Matches.Repository
$defaultHeaders = @{
Authorization = "Bearer ${bitbucketToken}"
Accept = "application/json"
}
# Uncomment the following code if you have to disable certificate validation (e. g. due to self-signed certificates)
#
# [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12
# Add-Type -TypeDefinition @"
# using System.Net;
# using System.Security.Cryptography.X509Certificates;
# public class TrustAllCertsPolicy : ICertificatePolicy
# {
# public bool CheckValidationResult(ServicePoint sp, X509Certificate cert, WebRequest req, int certProblem)
# {
# return true;
# }
# }
# "@
# [System.Net.ServicePointManager]::CertificatePolicy = New-Object -TypeName TrustAllCertsPolicy
function installDotnetOutdatedTool {
dotnet tool install --global dotnet-outdated-tool
}
function runDotnetOutdatedTool {
if ($solutions -eq $allProjectsIdentifier) {
createSolutionWithAllProjects
runDotnetOutdatedToolForSolution("${allProjectsIdentifier}.sln")
deleteSolutionWithAllProjects
}
else {
foreach ($solution in $solutions.Split(",")) {
runDotnetOutdatedToolForSolution($solution)
}
}
}
function createSolutionWithAllProjects {
& "dotnet" @("new", "sln", "--name", $allProjectsIdentifier)
Get-ChildItem -Path .\ -Filter *.csproj -Recurse -File -Name | ForEach-Object {
& "dotnet" @("sln", "${allProjectsIdentifier}.sln", "add", $_)
}
Write-Host "Created new solution '${allProjectsIdentifier}.sln' with all available C# projects"
}
function deleteSolutionWithAllProjects {
Remove-Item ".\${allProjectsIdentifier}.sln"
}
function runDotnetOutdatedToolForSolution($solution) {
$solution = $solution.Trim()
Write-Host "Running 'dotnet outdated' for '${solution}'"
$arguments = @("outdated", "--upgrade", "--version-lock", $versionLock)
if (![string]::IsNullOrWhiteSpace($packagesToExclude) -and ![string]::IsNullOrWhiteSpace($packagesToInclude)) {
Write-Error "Using both inclusion and exclusion of packages is not supported"
}
if (![string]::IsNullOrWhiteSpace($packagesToExclude)) {
foreach ($packageToExclude in $packagesToExclude.Split(",")) {
$packageToExclude = $packageToExclude.Trim()
$arguments += "--exclude"
$arguments += $packageToExclude
}
}
if (![string]::IsNullOrWhiteSpace($packagesToInclude)) {
foreach ($packageToInclude in $packagesToInclude.Split(",")) {
$packageToInclude = $packageToInclude.Trim()
$arguments += "--include"
$arguments += $packageToInclude
}
}
$arguments += ".\${solution}"
& "dotnet" $arguments
}
function hasGitChanges {
$hasChanges = git status --porcelain
return $hasChanges
}
function pushGitChangesToOrigin {
$bitbucketRepoUrlWithAuth = getRepoUrlWithAuthentication
# Why is `git push origin` not enough and authentication has to be specified?
# I developed and tested this script with and for the JetBrains CI solution TeamCity.
# TeamCity clones the repo, but somehow doesn't set up the cloned Git working directory 100% correctly.
# Therefore the Git credential manager pops up when using `git push origin'.
# This is solved by manually authenticating against Bitbucket.
git push $bitbucketRepoUrlWithAuth $newBranchWithUpdates
}
function getRepoUrlWithAuthentication {
$encodedUser = [System.Web.HttpUtility]::UrlEncode($bitbucketUser)
$encodedToken = [System.Web.HttpUtility]::UrlEncode($bitbucketToken)
return "${bitbucketProtocolWithSlash}${encodedUser}:${encodedToken}@${bitbucketHost}/scm/${bitbucketProject}/${bitbucketRepo}.git" -replace " ",""
}
function createNewGitBranch {
git checkout -b $newBranchWithUpdates
}
function commitGitChanges {
git add -A
git commit -m $commitMessage
}
function switchToDefaultBranch {
git checkout $defaultBranch
}
function persistGitChanges {
createNewGitBranch
commitGitChanges
pushGitChangesToOrigin
switchToDefaultBranch
}
function createPullRequestForChanges {
persistGitChanges
createPullRequest
}
function branchAsRefObject($branchName) {
return @{
displayId = $branchName
id = "refs/heads/${branchName}"
type = "BRANCH"
}
}
function getRepoId {
$url = "${bitbucketHostWithProtocol}/rest/api/1.0/projects/${bitbucketProject}/repos/${bitbucketRepo}"
$result = Invoke-RestMethod -Uri $url -Headers $defaultHeaders -ContentType "application/json"
return $result.id
}
function getDefaultReviewers ($sourceBranch, $targetBranch) {
$repoId = getRepoId
$url = "${bitbucketHostWithProtocol}/rest/default-reviewers/1.0/projects/${bitbucketProject}/repos/${bitbucketRepo}/reviewers?sourceRepoId=${repoId}&targetRepoId=${repoId}&sourceRefId=refs/heads/${sourceBranch}&targetRefId=refs/heads/${targetBranch}"
$results = Invoke-RestMethod -Uri $url -Headers $defaultHeaders -ContentType "application/json"
$reviewers = @()
foreach ($result in $results) {
$reviewers += @{
user = @{
name = $result.name
}
}
}
return $reviewers
}
function createPullRequest {
$url = "${bitbucketHostWithProtocol}/rest/api/1.0/projects/${bitbucketProject}/repos/${bitbucketRepo}/pull-requests"
$body = @{
fromRef = branchAsRefObject $newBranchWithUpdates
toRef = branchAsRefObject $defaultBranch
reviewers = getDefaultReviewers $newBranchWithUpdates $defaultBranch
title = $pullRequestUpdateIdentifier
} | ConvertTo-Json -Depth 4
Invoke-RestMethod -Method Post -Uri $url -Headers $defaultHeaders -Body $body -ContentType "application/json"
Write-Host "Created new PR from branch '${newBranchWithUpdates}' to branch '${defaultBranch}'"
}
function hasPendingPullRequest {
$url = "${bitbucketHostWithProtocol}/rest/api/1.0/projects/${bitbucketProject}/repos/${bitbucketRepo}/pull-requests"
$result = Invoke-RestMethod -Uri $url -Headers $defaultHeaders -ContentType "application/json"
foreach ($openPullRequest in $result.values) {
$pullRequestName = $openPullRequest.title
Write-Host "Found existing PR '${pullRequestName}'"
if ($pullRequestName -eq $pullRequestUpdateIdentifier) {
Write-Host "Current PR '${pullRequestName}' matches '${pullRequestUpdateIdentifier}'"
return $true
}
}
Write-Host "No PR matched '${pullRequestUpdateIdentifier}'"
return $false
}
function main {
if (hasPendingPullRequest) {
Write-Host "Pending update PR found, exiting"
return
}
installDotnetOutdatedTool
runDotnetOutdatedTool
if (hasGitChanges) {
Write-Host "Git changes detected, creating PR for changes"
if ($pushAndCreatePullRequest) {
createPullRequestForChanges
}
}
}
# Execute the main logic
main
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment