diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f413ca1..668b4f9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false matrix: os: - - ubuntu-20.04 + - ubuntu-22.04 - macos-latest - windows-latest diff --git a/azure-pipelines/Get-LibTemplateBasis.ps1 b/azure-pipelines/Get-LibTemplateBasis.ps1 new file mode 100644 index 0000000..2181c77 --- /dev/null +++ b/azure-pipelines/Get-LibTemplateBasis.ps1 @@ -0,0 +1,25 @@ +<# +.SYNOPSIS + Returns the name of the well-known branch in the Library.Template repository upon which HEAD is based. +#> +[CmdletBinding(SupportsShouldProcess = $true)] +Param( + [switch]$ErrorIfNotRelated +) + +# This list should be sorted in order of decreasing specificity. +$branchMarkers = @( + @{ commit = 'fd0a7b25ccf030bbd16880cca6efe009d5b1fffc'; branch = 'microbuild' }; + @{ commit = '05f49ce799c1f9cc696d53eea89699d80f59f833'; branch = 'main' }; +) + +foreach ($entry in $branchMarkers) { + if (git rev-list HEAD | Select-String -Pattern $entry.commit) { + return $entry.branch + } +} + +if ($ErrorIfNotRelated) { + Write-Error "Library.Template has not been previously merged with this repo. Please review https://github.com/AArnott/Library.Template/tree/main?tab=readme-ov-file#readme for instructions." + exit 1 +} diff --git a/azure-pipelines/build.yml b/azure-pipelines/build.yml index 102f222..16757b4 100644 --- a/azure-pipelines/build.yml +++ b/azure-pipelines/build.yml @@ -90,7 +90,7 @@ parameters: - name: linuxPool type: object default: - vmImage: ubuntu-20.04 + vmImage: ubuntu-22.04 - name: macOSPool type: object default: diff --git a/azure-pipelines/libtemplate-update.yml b/azure-pipelines/libtemplate-update.yml new file mode 100644 index 0000000..87302b0 --- /dev/null +++ b/azure-pipelines/libtemplate-update.yml @@ -0,0 +1,146 @@ +# This pipeline schedules regular merges of Library.Template into a repo that is based on it. +# Only Azure Repos are supported. GitHub support comes via a GitHub Actions workflow. + +trigger: none +pr: none +schedules: +- cron: "0 3 * * Mon" # Sun @ 8 or 9 PM Mountain Time (depending on DST) + displayName: Weekly trigger + branches: + include: + - main + always: true + +parameters: +- name: AutoComplete + displayName: Auto-complete pull request + type: boolean + default: false + +stages: +- stage: Merge + jobs: + - job: merge + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + fetchDepth: 0 + clean: true + - pwsh: | + $LibTemplateBranch = & ./azure-pipelines/Get-LibTemplateBasis.ps1 -ErrorIfNotRelated + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + + git fetch https://github.com/aarnott/Library.Template $LibTemplateBranch + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + $LibTemplateCommit = git rev-parse FETCH_HEAD + + if ((git rev-list FETCH_HEAD ^HEAD --count) -eq 0) { + Write-Host "There are no Library.Template updates to merge." + exit 0 + } + + $UpdateBranchName = 'auto/libtemplateUpdate' + git -c http.extraheader="AUTHORIZATION: bearer $(System.AccessToken)" push origin -f FETCH_HEAD:refs/heads/$UpdateBranchName + + Write-Host "Creating pull request" + $contentType = 'application/json'; + $headers = @{ Authorization = 'Bearer $(System.AccessToken)' }; + $rawRequest = @{ + sourceRefName = "refs/heads/$UpdateBranchName"; + targetRefName = "refs/heads/main"; + title = 'Merge latest Library.Template'; + description = "This merges the latest features and fixes from [Library.Template's $LibTemplateBranch branch](https://github.com/AArnott/Library.Template/tree/$LibTemplateBranch)."; + } + $request = ConvertTo-Json $rawRequest + + $prApiBaseUri = '$(System.TeamFoundationCollectionUri)/$(System.TeamProject)/_apis/git/repositories/$(Build.Repository.ID)/pullrequests' + $prCreationUri = $prApiBaseUri + "?api-version=6.0" + Write-Host "POST $prCreationUri" + Write-Host $request + + $prCreationResult = Invoke-RestMethod -uri $prCreationUri -method POST -Headers $headers -ContentType $contentType -Body $request + $prUrl = "$($prCreationResult.repository.webUrl)/pullrequest/$($prCreationResult.pullRequestId)" + Write-Host "Pull request: $prUrl" + $prApiBaseUri += "/$($prCreationResult.pullRequestId)" + + $SummaryPath = Join-Path '$(Agent.TempDirectory)' 'summary.md' + Set-Content -Path $SummaryPath -Value "[Insertion pull request]($prUrl)" + Write-Host "##vso[task.uploadsummary]$SummaryPath" + + # Tag the PR + $tagUri = "$prApiBaseUri/labels?api-version=7.0" + $rawRequest = @{ + name = 'auto-template-merge'; + } + $request = ConvertTo-Json $rawRequest + Invoke-RestMethod -uri $tagUri -method POST -Headers $headers -ContentType $contentType -Body $request | Out-Null + + # Add properties to the PR that we can programatically parse later. + Function Set-PRProperties($properties) { + $rawRequest = $properties.GetEnumerator() |% { + @{ + op = 'add' + path = "/$($_.key)" + from = $null + value = $_.value + } + } + $request = ConvertTo-Json $rawRequest + $setPrPropertyUri = "$prApiBaseUri/properties?api-version=7.0" + Write-Debug "$request" + $setPrPropertyResult = Invoke-RestMethod -uri $setPrPropertyUri -method PATCH -Headers $headers -ContentType 'application/json-patch+json' -Body $request -StatusCodeVariable setPrPropertyStatus -SkipHttpErrorCheck + if ($setPrPropertyStatus -ne 200) { + Write-Host "##vso[task.logissue type=warning]Failed to set pull request properties. Result: $setPrPropertyStatus. $($setPrPropertyResult.message)" + } + } + Write-Host "Setting pull request properties" + Set-PRProperties @{ + 'AutomatedMerge.SourceBranch' = $LibTemplateBranch + 'AutomatedMerge.SourceCommit' = $LibTemplateCommit + } + + # Add an *active* PR comment to warn users to *merge* the pull request instead of squash it. + $request = ConvertTo-Json @{ + comments = @( + @{ + parentCommentId = 0 + content = "Do **not** squash this pull request when completing it. You must *merge* it." + commentType = 'system' + } + ) + status = 'active' + } + $result = Invoke-RestMethod -uri "$prApiBaseUri/threads?api-version=7.1" -method POST -Headers $headers -ContentType $contentType -Body $request -StatusCodeVariable addCommentStatus -SkipHttpErrorCheck + if ($addCommentStatus -ne 200) { + Write-Host "##vso[task.logissue type=warning]Failed to post comment on pull request. Result: $addCommentStatus. $($result.message)" + } + + # Set auto-complete on the PR + if ('${{ parameters.AutoComplete }}' -eq 'True') { + Write-Host "Setting auto-complete" + $mergeMessage = "Merged PR $($prCreationResult.pullRequestId): " + $commitMessage + $rawRequest = @{ + autoCompleteSetBy = @{ + id = $prCreationResult.createdBy.id + }; + completionOptions = @{ + deleteSourceBranch = $true; + mergeCommitMessage = $mergeMessage; + mergeStrategy = 'noFastForward'; + }; + } + $request = ConvertTo-Json $rawRequest + Write-Host $request + $uri = "$($prApiBaseUri)?api-version=6.0" + $result = Invoke-RestMethod -uri $uri -method PATCH -Headers $headers -ContentType $contentType -Body $request -StatusCodeVariable autoCompleteStatus -SkipHttpErrorCheck + if ($autoCompleteStatus -ne 200) { + Write-Host "##vso[task.logissue type=warning]Failed to set auto-complete on pull request. Result: $autoCompleteStatus. $($result.message)" + } + } + + displayName: Create pull request diff --git a/tools/MergeFrom-Template.ps1 b/tools/MergeFrom-Template.ps1 new file mode 100644 index 0000000..c0d13dd --- /dev/null +++ b/tools/MergeFrom-Template.ps1 @@ -0,0 +1,79 @@ + +<# +.SYNOPSIS + Merges the latest changes from Library.Template into HEAD of this repo. +.PARAMETER LocalBranch + The name of the local branch to create at HEAD and use to merge into from Library.Template. +#> +[CmdletBinding(SupportsShouldProcess = $true)] +Param( + [string]$LocalBranch = "dev/$($env:USERNAME)/libtemplateUpdate" +) + +Function Spawn-Tool($command, $commandArgs, $workingDirectory, $allowFailures) { + if ($workingDirectory) { + Push-Location $workingDirectory + } + try { + if ($env:TF_BUILD) { + Write-Host "$pwd >" + Write-Host "##[command]$command $commandArgs" + } + else { + Write-Host "$command $commandArgs" -ForegroundColor Yellow + } + if ($commandArgs) { + & $command @commandArgs + } else { + Invoke-Expression $command + } + if ((!$allowFailures) -and ($LASTEXITCODE -ne 0)) { exit $LASTEXITCODE } + } + finally { + if ($workingDirectory) { + Pop-Location + } + } +} + +$remoteBranch = & $PSScriptRoot\..\azure-pipelines\Get-LibTemplateBasis.ps1 -ErrorIfNotRelated +if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE +} + +$LibTemplateUrl = 'https://github.com/aarnott/Library.Template' +Spawn-Tool 'git' ('fetch', $LibTemplateUrl, $remoteBranch) +$SourceCommit = git rev-parse FETCH_HEAD +$BaseBranch = Spawn-Tool 'git' ('branch', '--show-current') +$SourceCommitUrl = "$LibTemplateUrl/commit/$SourceCommit" + +# To reduce the odds of merge conflicts at this stage, we always move HEAD to the last successful merge. +$basis = Spawn-Tool 'git' ('rev-parse', 'HEAD') # TODO: consider improving this later + +Write-Host "Merging the $remoteBranch branch of Library.Template ($SourceCommit) into local repo $basis" -ForegroundColor Green + +Spawn-Tool 'git' ('checkout', '-b', $LocalBranch, $basis) $null $true +if ($LASTEXITCODE -eq 128) { + Spawn-Tool 'git' ('checkout', $LocalBranch) + Spawn-Tool 'git' ('merge', $basis) +} + +Spawn-Tool 'git' ('merge', 'FETCH_HEAD', '--no-ff', '-m', "Merge the $remoteBranch branch from $LibTemplateUrl`n`nSpecifically, this merges [$SourceCommit from that repo]($SourceCommitUrl).") +if ($LASTEXITCODE -eq 1) { + Write-Error "Merge conflict detected. Manual resolution required." + exit 1 +} +elseif ($LASTEXITCODE -ne 0) { + Write-Error "Merge failed with exit code $LASTEXITCODE." + exit $LASTEXITCODE +} + +$result = New-Object PSObject -Property @{ + BaseBranch = $BaseBranch # The original branch that was checked out when the script ran. + LocalBranch = $LocalBranch # The name of the local branch that was created before the merge. + SourceCommit = $SourceCommit # The commit from Library.Template that was merged in. + SourceBranch = $remoteBranch # The branch from Library.Template that was merged in. +} + +Write-Host $result +Write-Output $result