Sync eng/common directory with azure-sdk-tools repository (#46)
This commit is contained in:
parent
2708ca8f11
commit
7a02ab023b
65
eng/common/Extract-ReleaseNotes.ps1
Normal file
65
eng/common/Extract-ReleaseNotes.ps1
Normal file
@ -0,0 +1,65 @@
|
||||
# given a CHANGELOG.md file, extract the relevant info we need to decorate a release
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[String]$ChangeLogLocation,
|
||||
[String]$VersionString
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$RELEASE_TITLE_REGEX = "(?<releaseNoteTitle>^\#+.*(?<version>\b\d+\.\d+\.\d+([^0-9\s][^\s:]+)?))"
|
||||
|
||||
$releaseNotes = @{}
|
||||
$contentArrays = @{}
|
||||
if ($ChangeLogLocation.Length -eq 0)
|
||||
{
|
||||
return $releaseNotes
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$contents = Get-Content $ChangeLogLocation
|
||||
|
||||
# walk the document, finding where the version specifiers are and creating lists
|
||||
$version = ""
|
||||
foreach($line in $contents){
|
||||
if ($line -match $RELEASE_TITLE_REGEX)
|
||||
{
|
||||
$version = $matches["version"]
|
||||
$contentArrays[$version] = @()
|
||||
}
|
||||
|
||||
$contentArrays[$version] += $line
|
||||
}
|
||||
|
||||
# resolve each of discovered version specifier string arrays into real content
|
||||
foreach($key in $contentArrays.Keys)
|
||||
{
|
||||
$releaseNotes[$key] = New-Object PSObject -Property @{
|
||||
ReleaseVersion = $key
|
||||
ReleaseContent = $contentArrays[$key] -join [Environment]::NewLine
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Write-Host "Error parsing $ChangeLogLocation."
|
||||
Write-Host $_.Exception.Message
|
||||
}
|
||||
|
||||
if ([System.String]::IsNullOrEmpty($VersionString))
|
||||
{
|
||||
return $releaseNotes
|
||||
}
|
||||
else
|
||||
{
|
||||
if ($releaseNotes.ContainsKey($VersionString))
|
||||
{
|
||||
$releaseNotesForVersion = $releaseNotes[$VersionString].ReleaseContent
|
||||
$processedNotes = $releaseNotesForVersion -Split [Environment]::NewLine | where { $_ -notmatch $RELEASE_TITLE_REGEX }
|
||||
return $processedNotes -Join [Environment]::NewLine
|
||||
}
|
||||
Write-Error "Release Notes for the Specified version ${VersionString} was not found"
|
||||
exit 1
|
||||
}
|
||||
|
||||
356
eng/common/InterdependencyGraph.html
Normal file
356
eng/common/InterdependencyGraph.html
Normal file
@ -0,0 +1,356 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Interdependency Graph</title>
|
||||
<meta charset="utf-8">
|
||||
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.11.0/dist/cytoscape.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/dagre/0.8.4/dagre.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/cytoscape-dagre@2.2.2/cytoscape-dagre.min.js"></script>
|
||||
<script type="application/javascript">
|
||||
const renderGraph = (data) => {
|
||||
const config = {
|
||||
container: document.getElementById('cy'),
|
||||
elements: [],
|
||||
autounselectify: true,
|
||||
|
||||
layout: {
|
||||
name: 'dagre',
|
||||
ranker: 'tight-tree',
|
||||
nodeSep: 10,
|
||||
rankSep: 400,
|
||||
padding: 10
|
||||
},
|
||||
|
||||
style: [
|
||||
{
|
||||
selector: '.hidden',
|
||||
style: {
|
||||
'display': 'none'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'background-color': '#fff',
|
||||
'border-color': '#333',
|
||||
'border-width': '1px',
|
||||
'height': 'label',
|
||||
'label': 'data(label)',
|
||||
'padding': '8px',
|
||||
'shape': 'round-rectangle',
|
||||
'text-halign': 'center',
|
||||
'text-valign': 'center',
|
||||
'text-wrap': 'wrap',
|
||||
'width': 'label'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node.internal',
|
||||
style: {
|
||||
'background-color': '#7f7'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node.internalbinary',
|
||||
style: {
|
||||
'background-color': '#fb7'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node.collapsed',
|
||||
style: {
|
||||
'background-color': '#b7f'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node.search',
|
||||
style: {
|
||||
'background-color': '#ff7',
|
||||
'border-width': '6px',
|
||||
'display': 'element'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node.highlight',
|
||||
style: {
|
||||
'background-color': '#fff',
|
||||
'border-width': '6px',
|
||||
'display': 'element'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node.highlight.in',
|
||||
style: {
|
||||
'border-color': '#7bf'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node.highlight.out',
|
||||
style: {
|
||||
'border-color': '#f77'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node.highlight.source',
|
||||
style: {
|
||||
'border-color': '#f77'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node.highlight.internal',
|
||||
style: {
|
||||
'background-color': '#7f7'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node.highlight.internalbinary',
|
||||
style: {
|
||||
'background-color': '#fb7'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node.highlight.collapsed',
|
||||
style: {
|
||||
'background-color': '#b7f'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node.highlight.search',
|
||||
style: {
|
||||
'background-color': '#ff7'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'curve-style': 'bezier',
|
||||
'label': 'data(label)',
|
||||
'line-color': '#333',
|
||||
'target-arrow-color': '#333',
|
||||
'target-arrow-shape': 'triangle',
|
||||
'width': '1.5px'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'edge.highlight',
|
||||
style: {
|
||||
'display': 'element',
|
||||
'width': '6px'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'edge.highlight.in',
|
||||
style: {
|
||||
'line-color': '#7bf',
|
||||
'target-arrow-color': '#7bf'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'edge.highlight.out',
|
||||
style: {
|
||||
'line-color': '#f77',
|
||||
'target-arrow-color': '#f77'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Add the nodes
|
||||
for (const pkg of Object.keys(data)) {
|
||||
config.elements.push({
|
||||
data: {
|
||||
id: pkg,
|
||||
label: `${data[pkg].name}\n${data[pkg].version}`
|
||||
},
|
||||
classes: data[pkg].type
|
||||
})
|
||||
}
|
||||
|
||||
// Add the edges
|
||||
for (const pkg of Object.keys(data)) {
|
||||
for (const dep of data[pkg].deps) {
|
||||
const dest = `${dep.name}:${dep.version}`
|
||||
const edge = {
|
||||
data: {
|
||||
id: `${pkg}:${dest}`,
|
||||
source: pkg,
|
||||
target: dest,
|
||||
label: dep.label || ''
|
||||
}
|
||||
}
|
||||
config.elements.push(edge)
|
||||
}
|
||||
}
|
||||
|
||||
const cy = cytoscape(config)
|
||||
|
||||
cy.on('mouseover', 'node', event => {
|
||||
const element = event.target
|
||||
if (element.hasClass('pinned')) { return }
|
||||
|
||||
element.addClass('highlight source')
|
||||
element.outgoers().addClass('highlight out')
|
||||
element.incomers().addClass('highlight in')
|
||||
})
|
||||
|
||||
cy.on('mouseout', 'node', event => {
|
||||
const element = event.target
|
||||
if (element.hasClass('pinned')) { return }
|
||||
|
||||
element.removeClass('source')
|
||||
if (!element.hasClass('in') && !element.hasClass('out')) {
|
||||
element.removeClass('highlight')
|
||||
}
|
||||
|
||||
element.outgoers().forEach(e => {
|
||||
e.removeClass('out')
|
||||
if (!e.hasClass('in') && !e.hasClass('source')) {
|
||||
e.removeClass('highlight')
|
||||
}
|
||||
})
|
||||
|
||||
element.incomers().forEach(e => {
|
||||
e.removeClass('in')
|
||||
if (!e.hasClass('out') && !e.hasClass('source')) {
|
||||
e.removeClass('highlight')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
cy.on('cxttap', 'node', event => {
|
||||
const element = event.target
|
||||
if (!element.hasClass('pinned')) {
|
||||
element.addClass('pinned')
|
||||
|
||||
} else {
|
||||
element.removeClass('pinned')
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener('keydown', event => {
|
||||
if (document.activeElement.id === 'search') { return }
|
||||
|
||||
if (event.key === '-') {
|
||||
cy.nodes('.internal').forEach(node => {
|
||||
if (!node.hasClass('hidden')) {
|
||||
triggerCollapse(cy, node, true)
|
||||
}
|
||||
})
|
||||
} else if (event.key === '=') {
|
||||
cy.nodes('.internal').forEach(node => {
|
||||
triggerCollapse(cy, node, false)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
let searchTerm = ''
|
||||
document.getElementById('search').addEventListener('input', event => {
|
||||
const newValue = event.target.value
|
||||
if (searchTerm !== newValue) {
|
||||
searchTerm = newValue
|
||||
cy.nodes().removeClass('search')
|
||||
if (searchTerm.length > 0) {
|
||||
const matches = cy.nodes(`[label *= '${searchTerm}']`)
|
||||
matches.addClass('search')
|
||||
document.getElementById('matches').innerText = `Matches: ${matches.length}`
|
||||
} else {
|
||||
document.getElementById('matches').innerText = ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
cy.on('tap', 'node', event => {
|
||||
const element = event.target
|
||||
const collapse = !element.hasClass('collapsed')
|
||||
triggerCollapse(cy, element, collapse)
|
||||
element.emit('mouseout')
|
||||
element.emit('mouseover')
|
||||
})
|
||||
}
|
||||
|
||||
const triggerCollapse = (cy, element, collapse) => {
|
||||
if (element.outgoers().length === 0) { return }
|
||||
|
||||
if (collapse) {
|
||||
element.addClass('collapsed')
|
||||
} else {
|
||||
element.removeClass('collapsed')
|
||||
}
|
||||
|
||||
if (collapse) {
|
||||
element.outgoers('edge').addClass('hidden')
|
||||
const orphans = cy.filter(e => {
|
||||
return e.isNode() &&
|
||||
!e.hasClass('internal') &&
|
||||
!e.incomers('edge').some(g => !g.hasClass('hidden'))
|
||||
})
|
||||
orphans.forEach(o => {
|
||||
o.addClass('hidden')
|
||||
o.successors().addClass('hidden') // no-op when only one tier of external nodes are present
|
||||
})
|
||||
} else {
|
||||
element.outgoers().removeClass('hidden')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
margin: 10 auto;
|
||||
color: #333;
|
||||
font-weight: 300;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serf;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3em;
|
||||
font-weight: 300;
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
#cy {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: -1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.panel div {
|
||||
margin: 4px auto;
|
||||
}
|
||||
|
||||
.panel input {
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="panel">
|
||||
<h1>Dependency Graph</h1>
|
||||
<label for="search">Search:</label>
|
||||
<input id="search" type="search" autocomplete="off" size="64" />
|
||||
<div id="matches"></div>
|
||||
</div>
|
||||
<div id="cy"></div>
|
||||
<script type="application/javascript">
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const src = params.get("data") || "data.js";
|
||||
const script = document.createElement("script");
|
||||
script.src = src;
|
||||
script.async = false;
|
||||
script.addEventListener("load", () => renderGraph(data));
|
||||
script.addEventListener("error", e => {
|
||||
const dest = document.getElementsByClassName("panel")[0];
|
||||
dest.innerText = `Failed to load ${src}`;
|
||||
});
|
||||
document.head.appendChild(script);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
12
eng/common/README.md
Normal file
12
eng/common/README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Common Engineering System
|
||||
|
||||
The `eng/common` directory contains engineering files that are common across the various azure-sdk language repos.
|
||||
It should remain relatively small and only contain textual based files like scripts, configs, or templates. It
|
||||
should not contain binary files as they don't play well with git.
|
||||
|
||||
# Updating
|
||||
|
||||
Any updates to files in the `eng/common` directory should be made in the [azure-sdk-tools](https://github.com/azure/azure-sdk-tools) repo.
|
||||
All changes made will cause a PR to created in all subscribed azure-sdk language repos which will blindly replace all contents of
|
||||
the `eng/common` directory in that repo. For that reason do **NOT** make changes to files in this directory in the individual azure-sdk
|
||||
languages repos as they will be overwritten the next time an update is taken from the common azure-sdk-tools repo.
|
||||
17
eng/common/TestResources/New-TestResources.cmd
Normal file
17
eng/common/TestResources/New-TestResources.cmd
Normal file
@ -0,0 +1,17 @@
|
||||
@echo off
|
||||
|
||||
REM Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
REM Licensed under the MIT License.
|
||||
|
||||
setlocal
|
||||
|
||||
for /f "usebackq delims=" %%i in (`where pwsh 2^>nul`) do (
|
||||
set _cmd=%%i
|
||||
)
|
||||
|
||||
if "%_cmd%"=="" (
|
||||
echo Error: PowerShell not found. Please visit https://github.com/powershell/powershell for install instructions.
|
||||
exit /b 2
|
||||
)
|
||||
|
||||
call "%_cmd%" -NoLogo -NoProfile -File "%~dpn0.ps1" %*
|
||||
479
eng/common/TestResources/New-TestResources.ps1
Normal file
479
eng/common/TestResources/New-TestResources.ps1
Normal file
@ -0,0 +1,479 @@
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
#Requires -Version 6.0
|
||||
#Requires -PSEdition Core
|
||||
#Requires -Modules @{ModuleName='Az.Accounts'; ModuleVersion='1.6.4'}
|
||||
#Requires -Modules @{ModuleName='Az.Resources'; ModuleVersion='1.8.0'}
|
||||
|
||||
[CmdletBinding(DefaultParameterSetName = 'Default', SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
|
||||
param (
|
||||
# Limit $BaseName to enough characters to be under limit plus prefixes, and https://docs.microsoft.com/azure/architecture/best-practices/resource-naming.
|
||||
[Parameter(Mandatory = $true, Position = 0)]
|
||||
[ValidatePattern('^[-a-zA-Z0-9\.\(\)_]{0,80}(?<=[a-zA-Z0-9\(\)])$')]
|
||||
[string] $BaseName,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $ServiceDirectory,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidatePattern('^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$')]
|
||||
[string] $TestApplicationId,
|
||||
|
||||
[Parameter()]
|
||||
[string] $TestApplicationSecret,
|
||||
|
||||
[Parameter()]
|
||||
[ValidatePattern('^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$')]
|
||||
[string] $TestApplicationOid,
|
||||
|
||||
[Parameter(ParameterSetName = 'Provisioner', Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $TenantId,
|
||||
|
||||
[Parameter(ParameterSetName = 'Provisioner', Mandatory = $true)]
|
||||
[ValidatePattern('^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$')]
|
||||
[string] $ProvisionerApplicationId,
|
||||
|
||||
[Parameter(ParameterSetName = 'Provisioner', Mandatory = $true)]
|
||||
[string] $ProvisionerApplicationSecret,
|
||||
|
||||
[Parameter()]
|
||||
[ValidateRange(0, [int]::MaxValue)]
|
||||
[int] $DeleteAfterHours,
|
||||
|
||||
[Parameter()]
|
||||
[string] $Location = '',
|
||||
|
||||
[Parameter()]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $Environment = 'AzureCloud',
|
||||
|
||||
[Parameter()]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[hashtable] $AdditionalParameters,
|
||||
|
||||
[Parameter()]
|
||||
[switch] $CI = ($null -ne $env:SYSTEM_TEAMPROJECTID),
|
||||
|
||||
[Parameter()]
|
||||
[switch] $Force
|
||||
)
|
||||
|
||||
# By default stop for any error.
|
||||
if (!$PSBoundParameters.ContainsKey('ErrorAction')) {
|
||||
$ErrorActionPreference = 'Stop'
|
||||
}
|
||||
|
||||
function Log($Message) {
|
||||
Write-Host ('{0} - {1}' -f [DateTime]::Now.ToLongTimeString(), $Message)
|
||||
}
|
||||
|
||||
function Retry([scriptblock] $Action, [int] $Attempts = 5) {
|
||||
$attempt = 0
|
||||
$sleep = 5
|
||||
|
||||
while ($attempt -lt $Attempts) {
|
||||
try {
|
||||
$attempt++
|
||||
return $Action.Invoke()
|
||||
} catch {
|
||||
if ($attempt -lt $Attempts) {
|
||||
$sleep *= 2
|
||||
|
||||
Write-Warning "Attempt $attempt failed: $_. Trying again in $sleep seconds..."
|
||||
Start-Sleep -Seconds $sleep
|
||||
} else {
|
||||
Write-Error -ErrorRecord $_
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Support actions to invoke on exit.
|
||||
$exitActions = @({
|
||||
if ($exitActions.Count -gt 1) {
|
||||
Write-Verbose 'Running registered exit actions'
|
||||
}
|
||||
})
|
||||
|
||||
trap {
|
||||
# Like using try..finally in PowerShell, but without keeping track of more braces or tabbing content.
|
||||
$exitActions.Invoke()
|
||||
}
|
||||
|
||||
# Enumerate test resources to deploy. Fail if none found.
|
||||
$root = [System.IO.Path]::Combine("$PSScriptRoot/../sdk", $ServiceDirectory) | Resolve-Path
|
||||
$templateFileName = 'test-resources.json'
|
||||
$templateFiles = @()
|
||||
|
||||
Write-Verbose "Checking for '$templateFileName' files under '$root'"
|
||||
Get-ChildItem -Path $root -Filter $templateFileName -Recurse | ForEach-Object {
|
||||
$templateFile = $_.FullName
|
||||
|
||||
Write-Verbose "Found template '$templateFile'"
|
||||
$templateFiles += $templateFile
|
||||
}
|
||||
|
||||
if (!$templateFiles) {
|
||||
Write-Warning -Message "No template files found under '$root'"
|
||||
exit
|
||||
}
|
||||
|
||||
# If no location is specified use safe default locations for the given
|
||||
# environment. If no matching environment is found $Location remains an empty
|
||||
# string.
|
||||
if (!$Location) {
|
||||
$defaultLocations = @{
|
||||
'AzureCloud' = 'westus2';
|
||||
'AzureUSGovernment' = 'usgovvirginia';
|
||||
'AzureChinaCloud' = 'chinaeast2';
|
||||
}
|
||||
|
||||
if ($defaultLocations.ContainsKey($Environment)) {
|
||||
$Location = $defaultLocations[$Environment]
|
||||
} else {
|
||||
Write-Error "Location cannot be empty and there is no default location for Environment: '$Environment'"
|
||||
}
|
||||
|
||||
Write-Verbose "Location was not set. Using default location for environment: '$Location'"
|
||||
}
|
||||
|
||||
# Log in if requested; otherwise, the user is expected to already be authenticated via Connect-AzAccount.
|
||||
if ($ProvisionerApplicationId) {
|
||||
$null = Disable-AzContextAutosave -Scope Process
|
||||
|
||||
Log "Logging into service principal '$ProvisionerApplicationId'"
|
||||
$provisionerSecret = ConvertTo-SecureString -String $ProvisionerApplicationSecret -AsPlainText -Force
|
||||
$provisionerCredential = [System.Management.Automation.PSCredential]::new($ProvisionerApplicationId, $provisionerSecret)
|
||||
|
||||
$provisionerAccount = Retry {
|
||||
Connect-AzAccount -Tenant $TenantId -Credential $provisionerCredential -ServicePrincipal -Environment $Environment
|
||||
}
|
||||
|
||||
$exitActions += {
|
||||
Write-Verbose "Logging out of service principal '$($provisionerAccount.Context.Account)'"
|
||||
$null = Disconnect-AzAccount -AzureContext $provisionerAccount.Context
|
||||
}
|
||||
}
|
||||
|
||||
# Get test application OID from ID if not already provided.
|
||||
if ($TestApplicationId -and !$TestApplicationOid) {
|
||||
$testServicePrincipal = Retry {
|
||||
Get-AzADServicePrincipal -ApplicationId $TestApplicationId
|
||||
}
|
||||
|
||||
if ($testServicePrincipal -and $testServicePrincipal.Id) {
|
||||
$script:TestApplicationOid = $testServicePrincipal.Id
|
||||
}
|
||||
}
|
||||
|
||||
# Format the resource group name based on resource group naming recommendations and limitations.
|
||||
$resourceGroupName = if ($CI) {
|
||||
$BaseName = 't' + (New-Guid).ToString('n').Substring(0, 16)
|
||||
Write-Verbose "Generated base name '$BaseName' for CI build"
|
||||
|
||||
# If the ServiceDirectory is an absolute path use the last directory name
|
||||
# (e.g. D:\foo\bar\ -> bar)
|
||||
$serviceName = if (Split-Path -IsAbsolute $ServiceDirectory) {
|
||||
Split-Path -Leaf $ServiceDirectory
|
||||
} else {
|
||||
$ServiceDirectory
|
||||
}
|
||||
|
||||
"rg-{0}-$BaseName" -f ($serviceName -replace '[\\\/:]', '-').Substring(0, [Math]::Min($serviceName.Length, 90 - $BaseName.Length - 4)).Trim('-')
|
||||
} else {
|
||||
"rg-$BaseName"
|
||||
}
|
||||
|
||||
# Tag the resource group to be deleted after a certain number of hours if specified.
|
||||
$tags = @{
|
||||
Creator = if ($env:USER) { $env:USER } else { "${env:USERNAME}" }
|
||||
ServiceDirectory = $ServiceDirectory
|
||||
}
|
||||
|
||||
if ($PSBoundParameters.ContainsKey('DeleteAfterHours')) {
|
||||
$deleteAfter = [DateTime]::UtcNow.AddHours($DeleteAfterHours)
|
||||
$tags.Add('DeleteAfter', $deleteAfter.ToString('o'))
|
||||
}
|
||||
|
||||
if ($CI) {
|
||||
# Add tags for the current CI job.
|
||||
$tags += @{
|
||||
BuildId = "${env:BUILD_BUILDID}"
|
||||
BuildJob = "${env:AGENT_JOBNAME}"
|
||||
BuildNumber = "${env:BUILD_BUILDNUMBER}"
|
||||
BuildReason = "${env:BUILD_REASON}"
|
||||
}
|
||||
|
||||
# Set the resource group name variable.
|
||||
Write-Host "Setting variable 'AZURE_RESOURCEGROUP_NAME': $resourceGroupName"
|
||||
Write-Host "##vso[task.setvariable variable=AZURE_RESOURCEGROUP_NAME;]$resourceGroupName"
|
||||
}
|
||||
|
||||
Log "Creating resource group '$resourceGroupName' in location '$Location'"
|
||||
$resourceGroup = Retry {
|
||||
New-AzResourceGroup -Name "$resourceGroupName" -Location $Location -Tag $tags -Force:$Force
|
||||
}
|
||||
|
||||
if ($resourceGroup.ProvisioningState -eq 'Succeeded') {
|
||||
# New-AzResourceGroup would've written an error and stopped the pipeline by default anyway.
|
||||
Write-Verbose "Successfully created resource group '$($resourceGroup.ResourceGroupName)'"
|
||||
}
|
||||
|
||||
# Populate the template parameters and merge any additional specified.
|
||||
$templateParameters = @{
|
||||
baseName = $BaseName
|
||||
testApplicationId = $TestApplicationId
|
||||
testApplicationOid = "$TestApplicationOid"
|
||||
}
|
||||
|
||||
if ($TenantId) {
|
||||
$templateParameters.Add('tenantId', $TenantId)
|
||||
}
|
||||
if ($TestApplicationSecret) {
|
||||
$templateParameters.Add('testApplicationSecret', $TestApplicationSecret)
|
||||
}
|
||||
if ($AdditionalParameters) {
|
||||
$templateParameters += $AdditionalParameters
|
||||
}
|
||||
|
||||
# Try to detect the shell based on the parent process name (e.g. launch via shebang).
|
||||
$shell, $shellExportFormat = if (($parentProcessName = (Get-Process -Id $PID).Parent.ProcessName) -and $parentProcessName -eq 'cmd') {
|
||||
'cmd', 'set {0}={1}'
|
||||
} elseif (@('bash', 'csh', 'tcsh', 'zsh') -contains $parentProcessName) {
|
||||
'shell', 'export {0}={1}'
|
||||
} else {
|
||||
'PowerShell', '$env:{0} = ''{1}'''
|
||||
}
|
||||
|
||||
foreach ($templateFile in $templateFiles) {
|
||||
# Deployment fails if we pass in more parameters than are defined.
|
||||
Write-Verbose "Removing unnecessary parameters from template '$templateFile'"
|
||||
$templateJson = Get-Content -LiteralPath $templateFile | ConvertFrom-Json
|
||||
$templateParameterNames = $templateJson.parameters.PSObject.Properties.Name
|
||||
|
||||
$templateFileParameters = $templateParameters.Clone()
|
||||
foreach ($key in $templateParameters.Keys) {
|
||||
if ($templateParameterNames -notcontains $key) {
|
||||
Write-Verbose "Removing unnecessary parameter '$key'"
|
||||
$templateFileParameters.Remove($key)
|
||||
}
|
||||
}
|
||||
|
||||
$preDeploymentScript = $templateFile | Split-Path | Join-Path -ChildPath 'test-resources-pre.ps1'
|
||||
if (Test-Path $preDeploymentScript) {
|
||||
Log "Invoking pre-deployment script '$preDeploymentScript'"
|
||||
&$preDeploymentScript -ResourceGroupName $resourceGroupName @PSBoundParameters
|
||||
}
|
||||
|
||||
Log "Deploying template '$templateFile' to resource group '$($resourceGroup.ResourceGroupName)'"
|
||||
$deployment = Retry {
|
||||
New-AzResourceGroupDeployment -Name $BaseName -ResourceGroupName $resourceGroup.ResourceGroupName -TemplateFile $templateFile -TemplateParameterObject $templateFileParameters
|
||||
}
|
||||
|
||||
if ($deployment.ProvisioningState -eq 'Succeeded') {
|
||||
# New-AzResourceGroupDeployment would've written an error and stopped the pipeline by default anyway.
|
||||
Write-Verbose "Successfully deployed template '$templateFile' to resource group '$($resourceGroup.ResourceGroupName)'"
|
||||
}
|
||||
|
||||
if ($deployment.Outputs.Count -and !$CI) {
|
||||
# Write an extra new line to isolate the environment variables for easy reading.
|
||||
Log "Persist the following environment variables based on your detected shell ($shell):`n"
|
||||
}
|
||||
|
||||
$deploymentOutputs = @{}
|
||||
foreach ($key in $deployment.Outputs.Keys) {
|
||||
$variable = $deployment.Outputs[$key]
|
||||
|
||||
# Work around bug that makes the first few characters of environment variables be lowercase.
|
||||
$key = $key.ToUpperInvariant()
|
||||
|
||||
if ($variable.Type -eq 'String' -or $variable.Type -eq 'SecureString') {
|
||||
$deploymentOutputs[$key] = $variable.Value
|
||||
|
||||
if ($CI) {
|
||||
# Treat all ARM template output variables as secrets since "SecureString" variables do not set values.
|
||||
# In order to mask secrets but set environment variables for any given ARM template, we set variables twice as shown below.
|
||||
Write-Host "Setting variable '$key': ***"
|
||||
Write-Host "##vso[task.setvariable variable=_$key;issecret=true;]$($variable.Value)"
|
||||
Write-Host "##vso[task.setvariable variable=$key;]$($variable.Value)"
|
||||
} else {
|
||||
Write-Host ($shellExportFormat -f $key, $variable.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($key) {
|
||||
# Isolate the environment variables for easy reading.
|
||||
Write-Host "`n"
|
||||
$key = $null
|
||||
}
|
||||
|
||||
$postDeploymentScript = $templateFile | Split-Path | Join-Path -ChildPath 'test-resources-post.ps1'
|
||||
if (Test-Path $postDeploymentScript) {
|
||||
Log "Invoking post-deployment script '$postDeploymentScript'"
|
||||
&$postDeploymentScript -ResourceGroupName $resourceGroupName -DeploymentOutputs $deploymentOutputs @PSBoundParameters
|
||||
}
|
||||
}
|
||||
|
||||
$exitActions.Invoke()
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Deploys live test resources defined for a service directory to Azure.
|
||||
|
||||
.DESCRIPTION
|
||||
Deploys live test resouces specified in test-resources.json files to a resource
|
||||
group.
|
||||
|
||||
This script searches the directory specified in $ServiceDirectory recursively
|
||||
for files named test-resources.json. All found test-resources.json files will be
|
||||
deployed to the test resource group.
|
||||
|
||||
If no test-resources.json files are located the script exits without making
|
||||
changes to the Azure environment.
|
||||
|
||||
A service principal must first be created before this script is run and passed
|
||||
to $TestApplicationId and $TestApplicationSecret. Test resources will grant this
|
||||
service principal access.
|
||||
|
||||
This script uses credentials already specified in Connect-AzAccount or those
|
||||
specified in $ProvisionerApplicationId and $ProvisionerApplicationSecret.
|
||||
|
||||
.PARAMETER BaseName
|
||||
A name to use in the resource group and passed to the ARM template as 'baseName'.
|
||||
Limit $BaseName to enough characters to be under limit plus prefixes specified in
|
||||
the ARM template. See also https://docs.microsoft.com/azure/architecture/best-practices/resource-naming
|
||||
|
||||
Note: The value specified for this parameter will be overriden and generated
|
||||
by New-TestResources.ps1 if $CI is specified.
|
||||
|
||||
.PARAMETER ServiceDirectory
|
||||
A directory under 'sdk' in the repository root - optionally with subdirectories
|
||||
specified - in which to discover ARM templates named 'test-resources.json'.
|
||||
This can also be an absolute path or specify parent directories.
|
||||
|
||||
.PARAMETER TestApplicationId
|
||||
The AAD Application ID to authenticate the test runner against deployed
|
||||
resources. Passed to the ARM template as 'testApplicationId'.
|
||||
|
||||
This application is used by the test runner to execute tests against the
|
||||
live test resources.
|
||||
|
||||
.PARAMETER TestApplicationSecret
|
||||
Optional service principal secret (password) to authenticate the test runner
|
||||
against deployed resources. Passed to the ARM template as
|
||||
'testApplicationSecret'.
|
||||
|
||||
This application is used by the test runner to execute tests against the
|
||||
live test resources.
|
||||
|
||||
.PARAMETER TestApplicationOid
|
||||
Service Principal Object ID of the AAD Test application. This is used to assign
|
||||
permissions to the AAD application so it can access tested features on the live
|
||||
test resources (e.g. Role Assignments on resources). It is passed as to the ARM
|
||||
template as 'testApplicationOid'
|
||||
|
||||
For more information on the relationship between AAD Applications and Service
|
||||
Principals see: https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals
|
||||
|
||||
.PARAMETER TenantId
|
||||
The tenant ID of a service principal when a provisioner is specified. The same
|
||||
Tenant ID is used for Test Application and Provisioner Application. This value
|
||||
is passed to the ARM template as 'tenantId'.
|
||||
|
||||
.PARAMETER ProvisionerApplicationId
|
||||
The AAD Application ID used to provision test resources when a provisioner is
|
||||
specified.
|
||||
|
||||
If none is specified New-TestResources.ps1 uses the TestApplicationId.
|
||||
|
||||
This value is not passed to the ARM template.
|
||||
|
||||
.PARAMETER ProvisionerApplicationSecret
|
||||
A service principal secret (password) used to provision test resources when a
|
||||
provisioner is specified.
|
||||
|
||||
If none is specified New-TestResources.ps1 uses the TestApplicationSecret.
|
||||
|
||||
This value is not passed to the ARM template.
|
||||
|
||||
.PARAMETER DeleteAfterHours
|
||||
Optional. Positive integer number of hours from the current time to set the
|
||||
'DeleteAfter' tag on the created resource group. The computed value is a
|
||||
timestamp of the form "2020-03-04T09:07:04.3083910Z".
|
||||
|
||||
If this value is not specified no 'DeleteAfter' tag will be assigned to the
|
||||
created resource group.
|
||||
|
||||
An optional cleanup process can delete resource groups whose "DeleteAfter"
|
||||
timestamp is less than the current time.
|
||||
|
||||
This isused for CI automation.
|
||||
|
||||
.PARAMETER Location
|
||||
Optional location where resources should be created. By default this is
|
||||
'westus2'.
|
||||
|
||||
.PARAMETER AdditionalParameters
|
||||
Optional key-value pairs of parameters to pass to the ARM template(s).
|
||||
|
||||
.PARAMETER Environment
|
||||
Name of the cloud environment. The default is the Azure Public Cloud
|
||||
('PublicCloud')
|
||||
|
||||
.PARAMETER CI
|
||||
Indicates the script is run as part of a Continuous Integration / Continuous
|
||||
Deployment (CI/CD) build (only Azure Pipelines is currently supported).
|
||||
|
||||
.PARAMETER Force
|
||||
Force creation of resources instead of being prompted.
|
||||
|
||||
.EXAMPLE
|
||||
$subscriptionId = "REPLACE_WITH_SUBSCRIPTION_ID"
|
||||
Connect-AzAccount -Subscription $subscriptionId
|
||||
$testAadApp = New-AzADServicePrincipal -Role Owner -DisplayName 'azure-sdk-live-test-app'
|
||||
.\eng\common\LiveTestResources\New-TestResources.ps1 `
|
||||
-BaseName 'myalias' `
|
||||
-ServiceDirectory 'keyvault' `
|
||||
-TestApplicationId $testAadApp.ApplicationId.ToString() `
|
||||
-TestApplicationSecret (ConvertFrom-SecureString $testAadApp.Secret -AsPlainText)
|
||||
|
||||
Run this in a desktop environment to create new AAD apps and Service Principals
|
||||
that can be used to provision resources and run live tests.
|
||||
|
||||
Requires PowerShell 7 to use ConvertFrom-SecureString -AsPlainText or convert
|
||||
the SecureString to plaintext by another means.
|
||||
|
||||
.EXAMPLE
|
||||
eng/New-TestResources.ps1 `
|
||||
-BaseName 'Generated' `
|
||||
-ServiceDirectory '$(ServiceDirectory)' `
|
||||
-TenantId '$(TenantId)' `
|
||||
-ProvisionerApplicationId '$(ProvisionerId)' `
|
||||
-ProvisionerApplicationSecret '$(ProvisionerSecret)' `
|
||||
-TestApplicationId '$(TestAppId)' `
|
||||
-TestApplicationSecret '$(TestAppSecret)' `
|
||||
-DeleteAfterHours 24 `
|
||||
-CI `
|
||||
-Force `
|
||||
-Verbose
|
||||
|
||||
Run this in an Azure DevOps CI (with approrpiate variables configured) before
|
||||
executing live tests. The script will output variables as secrets (to enable
|
||||
log redaction).
|
||||
|
||||
.OUTPUTS
|
||||
Entries from the ARM templates' "output" section in environment variable syntax
|
||||
(e.g. $env:RESOURCE_NAME='<< resource name >>') that can be used for running
|
||||
live tests.
|
||||
|
||||
If run in -CI mode the environment variables will be output in syntax that Azure
|
||||
DevOps can consume.
|
||||
|
||||
.LINK
|
||||
Remove-TestResources.ps1
|
||||
#>
|
||||
410
eng/common/TestResources/New-TestResources.ps1.md
Normal file
410
eng/common/TestResources/New-TestResources.ps1.md
Normal file
@ -0,0 +1,410 @@
|
||||
---
|
||||
external help file: -help.xml
|
||||
Module Name:
|
||||
online version:
|
||||
schema: 2.0.0
|
||||
---
|
||||
|
||||
# New-TestResources.ps1
|
||||
|
||||
## SYNOPSIS
|
||||
Deploys live test resources defined for a service directory to Azure.
|
||||
|
||||
## SYNTAX
|
||||
|
||||
### Default (Default)
|
||||
```
|
||||
New-TestResources.ps1 [-BaseName] <String> -ServiceDirectory <String> -TestApplicationId <String>
|
||||
[-TestApplicationSecret <String>] [-TestApplicationOid <String>] [-DeleteAfterHours <Int32>]
|
||||
[-Location <String>] [-Environment <String>] [-AdditionalParameters <Hashtable>] [-CI] [-Force] [-WhatIf]
|
||||
[-Confirm] [<CommonParameters>]
|
||||
```
|
||||
|
||||
### Provisioner
|
||||
```
|
||||
New-TestResources.ps1 [-BaseName] <String> -ServiceDirectory <String> -TestApplicationId <String>
|
||||
[-TestApplicationSecret <String>] [-TestApplicationOid <String>] -TenantId <String>
|
||||
-ProvisionerApplicationId <String> -ProvisionerApplicationSecret <String> [-DeleteAfterHours <Int32>]
|
||||
[-Location <String>] [-Environment <String>] [-AdditionalParameters <Hashtable>] [-CI] [-Force] [-WhatIf]
|
||||
[-Confirm] [<CommonParameters>]
|
||||
```
|
||||
|
||||
## DESCRIPTION
|
||||
Deploys live test resouces specified in test-resources.json files to a resource
|
||||
group.
|
||||
|
||||
This script searches the directory specified in $ServiceDirectory recursively
|
||||
for files named test-resources.json.
|
||||
All found test-resources.json files will be
|
||||
deployed to the test resource group.
|
||||
|
||||
If no test-resources.json files are located the script exits without making
|
||||
changes to the Azure environment.
|
||||
|
||||
A service principal must first be created before this script is run and passed
|
||||
to $TestApplicationId and $TestApplicationSecret.
|
||||
Test resources will grant this
|
||||
service principal access.
|
||||
|
||||
This script uses credentials already specified in Connect-AzAccount or those
|
||||
specified in $ProvisionerApplicationId and $ProvisionerApplicationSecret.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
### EXAMPLE 1
|
||||
```
|
||||
$subscriptionId = "REPLACE_WITH_SUBSCRIPTION_ID"
|
||||
Connect-AzAccount -Subscription $subscriptionId
|
||||
$testAadApp = New-AzADServicePrincipal -Role Owner -DisplayName 'azure-sdk-live-test-app'
|
||||
.\eng\common\LiveTestResources\New-TestResources.ps1 `
|
||||
-BaseName 'myalias' `
|
||||
-ServiceDirectory 'keyvault' `
|
||||
-TestApplicationId $testAadApp.ApplicationId.ToString() `
|
||||
-TestApplicationSecret (ConvertFrom-SecureString $testAadApp.Secret -AsPlainText)
|
||||
```
|
||||
|
||||
Run this in a desktop environment to create new AAD apps and Service Principals
|
||||
that can be used to provision resources and run live tests.
|
||||
|
||||
Requires PowerShell 7 to use ConvertFrom-SecureString -AsPlainText or convert
|
||||
the SecureString to plaintext by another means.
|
||||
|
||||
### EXAMPLE 2
|
||||
```
|
||||
eng/New-TestResources.ps1 `
|
||||
-BaseName 'Generated' `
|
||||
-ServiceDirectory '$(ServiceDirectory)' `
|
||||
-TenantId '$(TenantId)' `
|
||||
-ProvisionerApplicationId '$(ProvisionerId)' `
|
||||
-ProvisionerApplicationSecret '$(ProvisionerSecret)' `
|
||||
-TestApplicationId '$(TestAppId)' `
|
||||
-TestApplicationSecret '$(TestAppSecret)' `
|
||||
-DeleteAfterHours 24 `
|
||||
-CI `
|
||||
-Force `
|
||||
-Verbose
|
||||
```
|
||||
|
||||
Run this in an Azure DevOps CI (with approrpiate variables configured) before
|
||||
executing live tests.
|
||||
The script will output variables as secrets (to enable
|
||||
log redaction).
|
||||
|
||||
## PARAMETERS
|
||||
|
||||
### -BaseName
|
||||
A name to use in the resource group and passed to the ARM template as 'baseName'.
|
||||
Limit $BaseName to enough characters to be under limit plus prefixes specified in
|
||||
the ARM template.
|
||||
See also https://docs.microsoft.com/azure/architecture/best-practices/resource-naming
|
||||
|
||||
Note: The value specified for this parameter will be overriden and generated
|
||||
by New-TestResources.ps1 if $CI is specified.
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: True
|
||||
Position: 1
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -ServiceDirectory
|
||||
A directory under 'sdk' in the repository root - optionally with subdirectories
|
||||
specified - in which to discover ARM templates named 'test-resources.json'.
|
||||
This can also be an absolute path or specify parent directories.
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: True
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -TestApplicationId
|
||||
The AAD Application ID to authenticate the test runner against deployed
|
||||
resources.
|
||||
Passed to the ARM template as 'testApplicationId'.
|
||||
|
||||
This application is used by the test runner to execute tests against the
|
||||
live test resources.
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: True
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -TestApplicationSecret
|
||||
Optional service principal secret (password) to authenticate the test runner
|
||||
against deployed resources.
|
||||
Passed to the ARM template as
|
||||
'testApplicationSecret'.
|
||||
|
||||
This application is used by the test runner to execute tests against the
|
||||
live test resources.
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -TestApplicationOid
|
||||
Service Principal Object ID of the AAD Test application.
|
||||
This is used to assign
|
||||
permissions to the AAD application so it can access tested features on the live
|
||||
test resources (e.g.
|
||||
Role Assignments on resources).
|
||||
It is passed as to the ARM
|
||||
template as 'testApplicationOid'
|
||||
|
||||
For more information on the relationship between AAD Applications and Service
|
||||
Principals see: https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -TenantId
|
||||
The tenant ID of a service principal when a provisioner is specified.
|
||||
The same
|
||||
Tenant ID is used for Test Application and Provisioner Application.
|
||||
This value
|
||||
is passed to the ARM template as 'tenantId'.
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: Provisioner
|
||||
Aliases:
|
||||
|
||||
Required: True
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -ProvisionerApplicationId
|
||||
The AAD Application ID used to provision test resources when a provisioner is
|
||||
specified.
|
||||
|
||||
If none is specified New-TestResources.ps1 uses the TestApplicationId.
|
||||
|
||||
This value is not passed to the ARM template.
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: Provisioner
|
||||
Aliases:
|
||||
|
||||
Required: True
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -ProvisionerApplicationSecret
|
||||
A service principal secret (password) used to provision test resources when a
|
||||
provisioner is specified.
|
||||
|
||||
If none is specified New-TestResources.ps1 uses the TestApplicationSecret.
|
||||
|
||||
This value is not passed to the ARM template.
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: Provisioner
|
||||
Aliases:
|
||||
|
||||
Required: True
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -DeleteAfterHours
|
||||
Optional.
|
||||
Positive integer number of hours from the current time to set the
|
||||
'DeleteAfter' tag on the created resource group.
|
||||
The computed value is a
|
||||
timestamp of the form "2020-03-04T09:07:04.3083910Z".
|
||||
|
||||
If this value is not specified no 'DeleteAfter' tag will be assigned to the
|
||||
created resource group.
|
||||
|
||||
An optional cleanup process can delete resource groups whose "DeleteAfter"
|
||||
timestamp is less than the current time.
|
||||
|
||||
This isused for CI automation.
|
||||
|
||||
```yaml
|
||||
Type: Int32
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
Position: Named
|
||||
Default value: 0
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -Location
|
||||
Optional location where resources should be created.
|
||||
By default this is
|
||||
'westus2'.
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
Position: Named
|
||||
Default value: Westus2
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -Environment
|
||||
Name of the cloud environment.
|
||||
The default is the Azure Public Cloud
|
||||
('PublicCloud')
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
Position: Named
|
||||
Default value: AzureCloud
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -AdditionalParameters
|
||||
Optional key-value pairs of parameters to pass to the ARM template(s).
|
||||
|
||||
```yaml
|
||||
Type: Hashtable
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -CI
|
||||
Indicates the script is run as part of a Continuous Integration / Continuous
|
||||
Deployment (CI/CD) build (only Azure Pipelines is currently supported).
|
||||
|
||||
```yaml
|
||||
Type: SwitchParameter
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
Position: Named
|
||||
Default value: ($null -ne $env:SYSTEM_TEAMPROJECTID)
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -Force
|
||||
Force creation of resources instead of being prompted.
|
||||
|
||||
```yaml
|
||||
Type: SwitchParameter
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
Position: Named
|
||||
Default value: False
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -WhatIf
|
||||
Shows what would happen if the cmdlet runs.
|
||||
The cmdlet is not run.
|
||||
|
||||
```yaml
|
||||
Type: SwitchParameter
|
||||
Parameter Sets: (All)
|
||||
Aliases: wi
|
||||
|
||||
Required: False
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -Confirm
|
||||
Prompts you for confirmation before running the cmdlet.
|
||||
|
||||
```yaml
|
||||
Type: SwitchParameter
|
||||
Parameter Sets: (All)
|
||||
Aliases: cf
|
||||
|
||||
Required: False
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### CommonParameters
|
||||
This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216).
|
||||
|
||||
## OUTPUTS
|
||||
|
||||
### Entries from the ARM templates' "output" section in environment variable syntax
|
||||
### (e.g. $env:RESOURCE_NAME='<< resource name >>') that can be used for running
|
||||
### live tests.
|
||||
### If run in -CI mode the environment variables will be output in syntax that Azure
|
||||
### DevOps can consume.
|
||||
## NOTES
|
||||
|
||||
## RELATED LINKS
|
||||
|
||||
[Remove-TestResources.ps1](./New-TestResources.ps1.md)
|
||||
|
||||
29
eng/common/TestResources/README.md
Normal file
29
eng/common/TestResources/README.md
Normal file
@ -0,0 +1,29 @@
|
||||
# Live Test Resource Management
|
||||
|
||||
Live test runs require pre-existing resources in Azure. This set of PowerShell
|
||||
commands automates creation and teardown of live test resources for Desktop and
|
||||
CI scenarios.
|
||||
|
||||
* [New-TestResources.ps1](./New-TestResources.ps1.md) - Create new test resources
|
||||
for the given service.
|
||||
* [Remove-TestResources.ps1](./New-TestResources.ps1.md) - Deletes resources
|
||||
|
||||
## On the Desktop
|
||||
|
||||
Run `New-TestResources.ps1` on your desktop to create live test resources for a
|
||||
given service (e.g. Storage, Key Vault, etc.). The command will output
|
||||
environment variables that need to be set when running the live tests.
|
||||
|
||||
See examples for how to create the needed Service Principals and execute live
|
||||
tests.
|
||||
|
||||
## In CI
|
||||
|
||||
The `New-TestResources.ps1` script is invoked on each test job to create an
|
||||
isolated environment for live tests. Test resource isolation makes it easier to
|
||||
parallelize test runs.
|
||||
|
||||
## Other
|
||||
|
||||
PowerShell markdown documentation created with
|
||||
[PlatyPS](https://github.com/PowerShell/platyPS)
|
||||
17
eng/common/TestResources/Remove-TestResources.cmd
Normal file
17
eng/common/TestResources/Remove-TestResources.cmd
Normal file
@ -0,0 +1,17 @@
|
||||
@echo off
|
||||
|
||||
REM Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
REM Licensed under the MIT License.
|
||||
|
||||
setlocal
|
||||
|
||||
for /f "usebackq delims=" %%i in (`where pwsh 2^>nul`) do (
|
||||
set _cmd=%%i
|
||||
)
|
||||
|
||||
if "%_cmd%"=="" (
|
||||
echo Error: PowerShell not found. Please visit https://github.com/powershell/powershell for install instructions.
|
||||
exit /b 2
|
||||
)
|
||||
|
||||
call "%_cmd%" -NoLogo -NoProfile -File "%~dpn0.ps1" %*
|
||||
173
eng/common/TestResources/Remove-TestResources.ps1
Normal file
173
eng/common/TestResources/Remove-TestResources.ps1
Normal file
@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
#Requires -Version 6.0
|
||||
#Requires -PSEdition Core
|
||||
#Requires -Modules @{ModuleName='Az.Accounts'; ModuleVersion='1.6.4'}
|
||||
#Requires -Modules @{ModuleName='Az.Resources'; ModuleVersion='1.8.0'}
|
||||
|
||||
[CmdletBinding(DefaultParameterSetName = 'Default', SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
|
||||
param (
|
||||
# Limit $BaseName to enough characters to be under limit plus prefixes, and https://docs.microsoft.com/azure/architecture/best-practices/resource-naming.
|
||||
[Parameter(ParameterSetName = 'Default', Mandatory = $true, Position = 0)]
|
||||
[Parameter(ParameterSetName = 'Default+Provisioner', Mandatory = $true, Position = 0)]
|
||||
[ValidatePattern('^[-a-zA-Z0-9\.\(\)_]{0,80}(?<=[a-zA-Z0-9\(\)])$')]
|
||||
[string] $BaseName,
|
||||
|
||||
[Parameter(ParameterSetName = 'ResourceGroup', Mandatory = $true)]
|
||||
[Parameter(ParameterSetName = 'ResourceGroup+Provisioner', Mandatory = $true)]
|
||||
[string] $ResourceGroupName,
|
||||
|
||||
[Parameter(ParameterSetName = 'Default+Provisioner', Mandatory = $true)]
|
||||
[Parameter(ParameterSetName = 'ResourceGroup+Provisioner', Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $TenantId,
|
||||
|
||||
[Parameter(ParameterSetName = 'Default+Provisioner', Mandatory = $true)]
|
||||
[Parameter(ParameterSetName = 'ResourceGroup+Provisioner', Mandatory = $true)]
|
||||
[ValidatePattern('^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$')]
|
||||
[string] $ProvisionerApplicationId,
|
||||
|
||||
[Parameter(ParameterSetName = 'Default+Provisioner', Mandatory = $true)]
|
||||
[Parameter(ParameterSetName = 'ResourceGroup+Provisioner', Mandatory = $true)]
|
||||
[string] $ProvisionerApplicationSecret,
|
||||
|
||||
[Parameter()]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $Environment = 'AzureCloud',
|
||||
|
||||
[Parameter()]
|
||||
[switch] $Force
|
||||
)
|
||||
|
||||
# By default stop for any error.
|
||||
if (!$PSBoundParameters.ContainsKey('ErrorAction')) {
|
||||
$ErrorActionPreference = 'Stop'
|
||||
}
|
||||
|
||||
function Log($Message) {
|
||||
Write-Host ('{0} - {1}' -f [DateTime]::Now.ToLongTimeString(), $Message)
|
||||
}
|
||||
|
||||
function Retry([scriptblock] $Action, [int] $Attempts = 5) {
|
||||
$attempt = 0
|
||||
$sleep = 5
|
||||
|
||||
while ($attempt -lt $Attempts) {
|
||||
try {
|
||||
$attempt++
|
||||
return $Action.Invoke()
|
||||
} catch {
|
||||
if ($attempt -lt $Attempts) {
|
||||
$sleep *= 2
|
||||
|
||||
Write-Warning "Attempt $attempt failed: $_. Trying again in $sleep seconds..."
|
||||
Start-Sleep -Seconds $sleep
|
||||
} else {
|
||||
Write-Error -ErrorRecord $_
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Support actions to invoke on exit.
|
||||
$exitActions = @({
|
||||
if ($exitActions.Count -gt 1) {
|
||||
Write-Verbose 'Running registered exit actions.'
|
||||
}
|
||||
})
|
||||
|
||||
trap {
|
||||
# Like using try..finally in PowerShell, but without keeping track of more braces or tabbing content.
|
||||
$exitActions.Invoke()
|
||||
}
|
||||
|
||||
if ($ProvisionerApplicationId) {
|
||||
$null = Disable-AzContextAutosave -Scope Process
|
||||
|
||||
Log "Logging into service principal '$ProvisionerApplicationId'"
|
||||
$provisionerSecret = ConvertTo-SecureString -String $ProvisionerApplicationSecret -AsPlainText -Force
|
||||
$provisionerCredential = [System.Management.Automation.PSCredential]::new($ProvisionerApplicationId, $provisionerSecret)
|
||||
$provisionerAccount = Retry {
|
||||
Connect-AzAccount -Tenant $TenantId -Credential $provisionerCredential -ServicePrincipal -Environment $Environment
|
||||
}
|
||||
|
||||
$exitActions += {
|
||||
Write-Verbose "Logging out of service principal '$($provisionerAccount.Context.Account)'"
|
||||
$null = Disconnect-AzAccount -AzureContext $provisionerAccount.Context
|
||||
}
|
||||
}
|
||||
|
||||
if (!$ResourceGroupName) {
|
||||
# Format the resource group name like in New-TestResources.ps1.
|
||||
$ResourceGroupName = "rg-$BaseName"
|
||||
}
|
||||
|
||||
Log "Deleting resource group '$ResourceGroupName'"
|
||||
if (Retry { Remove-AzResourceGroup -Name "$ResourceGroupName" -Force:$Force }) {
|
||||
Write-Verbose "Successfully deleted resource group '$ResourceGroupName'"
|
||||
}
|
||||
|
||||
$exitActions.Invoke()
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Deletes the resource group deployed for a service directory from Azure.
|
||||
|
||||
.DESCRIPTION
|
||||
Removes a resource group and all its resources previously deployed using
|
||||
New-TestResources.ps1.
|
||||
|
||||
If you are not currently logged into an account in the Az PowerShell module,
|
||||
you will be asked to log in with Connect-AzAccount. Alternatively, you (or a
|
||||
build pipeline) can pass $ProvisionerApplicationId and
|
||||
$ProvisionerApplicationSecret to authenticate a service principal with access to
|
||||
create resources.
|
||||
|
||||
.PARAMETER BaseName
|
||||
A name to use in the resource group and passed to the ARM template as 'baseName'.
|
||||
This will delete the resource group named 'rg-<baseName>'
|
||||
|
||||
.PARAMETER ResourceGroupName
|
||||
The name of the resource group to delete.
|
||||
|
||||
.PARAMETER TenantId
|
||||
The tenant ID of a service principal when a provisioner is specified.
|
||||
|
||||
.PARAMETER ProvisionerApplicationId
|
||||
A service principal ID to provision test resources when a provisioner is specified.
|
||||
|
||||
.PARAMETER ProvisionerApplicationSecret
|
||||
A service principal secret (password) to provision test resources when a provisioner is specified.
|
||||
|
||||
.PARAMETER Environment
|
||||
Name of the cloud environment. The default is the Azure Public Cloud
|
||||
('PublicCloud')
|
||||
|
||||
.PARAMETER Force
|
||||
Force removal of resource group without asking for user confirmation
|
||||
|
||||
.EXAMPLE
|
||||
./Remove-TestResources.ps1 -BaseName uuid123 -Force
|
||||
|
||||
Use the currently logged-in account to delete the resource group by the name of
|
||||
'rg-uuid123'
|
||||
|
||||
.EXAMPLE
|
||||
eng/Remove-TestResources.ps1 `
|
||||
-ResourceGroupName "${env:AZURE_RESOURCEGROUP_NAME}" `
|
||||
-TenantId '$(TenantId)' `
|
||||
-ProvisionerApplicationId '$(AppId)' `
|
||||
-ProvisionerApplicationSecret '$(AppSecret)' `
|
||||
-Force `
|
||||
-Verbose `
|
||||
|
||||
When run in the context of an Azure DevOps pipeline, this script removes the
|
||||
resource group whose name is stored in the environment variable
|
||||
AZURE_RESOURCEGROUP_NAME.
|
||||
|
||||
.LINK
|
||||
New-TestResources.ps1
|
||||
#>
|
||||
224
eng/common/TestResources/Remove-TestResources.ps1.md
Normal file
224
eng/common/TestResources/Remove-TestResources.ps1.md
Normal file
@ -0,0 +1,224 @@
|
||||
---
|
||||
external help file: -help.xml
|
||||
Module Name:
|
||||
online version:
|
||||
schema: 2.0.0
|
||||
---
|
||||
|
||||
# Remove-TestResources.ps1
|
||||
|
||||
## SYNOPSIS
|
||||
Deletes the resource group deployed for a service directory from Azure.
|
||||
|
||||
## SYNTAX
|
||||
|
||||
### Default (Default)
|
||||
```
|
||||
Remove-TestResources.ps1 [-BaseName] <String> [-Environment <String>] [-Force] [-WhatIf] [-Confirm]
|
||||
[<CommonParameters>]
|
||||
```
|
||||
|
||||
### Default+Provisioner
|
||||
```
|
||||
Remove-TestResources.ps1 [-BaseName] <String> -TenantId <String> -ProvisionerApplicationId <String>
|
||||
-ProvisionerApplicationSecret <String> [-Environment <String>] [-Force] [-WhatIf] [-Confirm]
|
||||
[<CommonParameters>]
|
||||
```
|
||||
|
||||
### ResourceGroup+Provisioner
|
||||
```
|
||||
Remove-TestResources.ps1 -ResourceGroupName <String> -TenantId <String> -ProvisionerApplicationId <String>
|
||||
-ProvisionerApplicationSecret <String> [-Environment <String>] [-Force] [-WhatIf] [-Confirm]
|
||||
[<CommonParameters>]
|
||||
```
|
||||
|
||||
### ResourceGroup
|
||||
```
|
||||
Remove-TestResources.ps1 -ResourceGroupName <String> [-Environment <String>] [-Force] [-WhatIf] [-Confirm]
|
||||
[<CommonParameters>]
|
||||
```
|
||||
|
||||
## DESCRIPTION
|
||||
Removes a resource group and all its resources previously deployed using
|
||||
New-TestResources.ps1.
|
||||
|
||||
If you are not currently logged into an account in the Az PowerShell module,
|
||||
you will be asked to log in with Connect-AzAccount.
|
||||
Alternatively, you (or a
|
||||
build pipeline) can pass $ProvisionerApplicationId and
|
||||
$ProvisionerApplicationSecret to authenticate a service principal with access to
|
||||
create resources.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
### EXAMPLE 1
|
||||
```
|
||||
./Remove-TestResources.ps1 -BaseName uuid123 -Force
|
||||
```
|
||||
|
||||
Use the currently logged-in account to delete the resource group by the name of
|
||||
'rg-uuid123'
|
||||
|
||||
### EXAMPLE 2
|
||||
```
|
||||
eng/Remove-TestResources.ps1 `
|
||||
-ResourceGroupName "${env:AZURE_RESOURCEGROUP_NAME}" `
|
||||
-TenantId '$(TenantId)' `
|
||||
-ProvisionerApplicationId '$(AppId)' `
|
||||
-ProvisionerApplicationSecret '$(AppSecret)' `
|
||||
-Force `
|
||||
-Verbose `
|
||||
```
|
||||
|
||||
When run in the context of an Azure DevOps pipeline, this script removes the
|
||||
resource group whose name is stored in the environment variable
|
||||
AZURE_RESOURCEGROUP_NAME.
|
||||
|
||||
## PARAMETERS
|
||||
|
||||
### -BaseName
|
||||
A name to use in the resource group and passed to the ARM template as 'baseName'.
|
||||
This will delete the resource group named 'rg-\<baseName\>'
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: Default, Default+Provisioner
|
||||
Aliases:
|
||||
|
||||
Required: True
|
||||
Position: 1
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -ResourceGroupName
|
||||
The name of the resource group to delete.
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: ResourceGroup+Provisioner, ResourceGroup
|
||||
Aliases:
|
||||
|
||||
Required: True
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -TenantId
|
||||
The tenant ID of a service principal when a provisioner is specified.
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: Default+Provisioner, ResourceGroup+Provisioner
|
||||
Aliases:
|
||||
|
||||
Required: True
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -ProvisionerApplicationId
|
||||
A service principal ID to provision test resources when a provisioner is specified.
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: Default+Provisioner, ResourceGroup+Provisioner
|
||||
Aliases:
|
||||
|
||||
Required: True
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -ProvisionerApplicationSecret
|
||||
A service principal secret (password) to provision test resources when a provisioner is specified.
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: Default+Provisioner, ResourceGroup+Provisioner
|
||||
Aliases:
|
||||
|
||||
Required: True
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -Environment
|
||||
Name of the cloud environment.
|
||||
The default is the Azure Public Cloud
|
||||
('PublicCloud')
|
||||
|
||||
```yaml
|
||||
Type: String
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
Position: Named
|
||||
Default value: AzureCloud
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -Force
|
||||
Force removal of resource group without asking for user confirmation
|
||||
|
||||
```yaml
|
||||
Type: SwitchParameter
|
||||
Parameter Sets: (All)
|
||||
Aliases:
|
||||
|
||||
Required: False
|
||||
Position: Named
|
||||
Default value: False
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -WhatIf
|
||||
Shows what would happen if the cmdlet runs.
|
||||
The cmdlet is not run.
|
||||
|
||||
```yaml
|
||||
Type: SwitchParameter
|
||||
Parameter Sets: (All)
|
||||
Aliases: wi
|
||||
|
||||
Required: False
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### -Confirm
|
||||
Prompts you for confirmation before running the cmdlet.
|
||||
|
||||
```yaml
|
||||
Type: SwitchParameter
|
||||
Parameter Sets: (All)
|
||||
Aliases: cf
|
||||
|
||||
Required: False
|
||||
Position: Named
|
||||
Default value: None
|
||||
Accept pipeline input: False
|
||||
Accept wildcard characters: False
|
||||
```
|
||||
|
||||
### CommonParameters
|
||||
This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216).
|
||||
|
||||
## RELATED LINKS
|
||||
|
||||
[New-TestResources.ps1](./New-TestResources.ps1.md)
|
||||
|
||||
73
eng/common/TestResources/deploy-test-resources.yml
Normal file
73
eng/common/TestResources/deploy-test-resources.yml
Normal file
@ -0,0 +1,73 @@
|
||||
# Deploys resources to a cloud type specified by the variable (not parameter)
|
||||
# 'CloudType'. Use this as part of a matrix to deploy resources to a particular
|
||||
# cloud instance. Normally we would use template parameters instead of variables
|
||||
# but matrix variables are not available during template expansion so any
|
||||
# benefits of parameters are lost.
|
||||
|
||||
parameters:
|
||||
ServiceDirectory: not-set
|
||||
ArmTemplateParameters: '@{}'
|
||||
DeleteAfterHours: 24
|
||||
Location: ''
|
||||
|
||||
steps:
|
||||
# New-TestResources command requires Az module
|
||||
- pwsh: Install-Module -Name Az -Scope CurrentUser -AllowClobber -Force -Verbose
|
||||
displayName: Install Azure PowerShell module
|
||||
|
||||
- pwsh: >
|
||||
eng/common/TestResources/New-TestResources.ps1
|
||||
-BaseName 'Generated'
|
||||
-ServiceDirectory '${{ parameters.ServiceDirectory }}'
|
||||
-TenantId '$(aad-azure-sdk-test-tenant-id)'
|
||||
-TestApplicationId '$(aad-azure-sdk-test-client-id)'
|
||||
-TestApplicationSecret '$(aad-azure-sdk-test-client-secret)'
|
||||
-ProvisionerApplicationId '$(aad-azure-sdk-test-client-id)'
|
||||
-ProvisionerApplicationSecret '$(aad-azure-sdk-test-client-secret)'
|
||||
-AdditionalParameters ${{ parameters.ArmTemplateParameters }}
|
||||
-DeleteAfterHours ${{ parameters.DeleteAfterHours }}
|
||||
-Location '${{ parameters.Location }}'
|
||||
-Environment 'AzureCloud'
|
||||
-CI
|
||||
-Force
|
||||
-Verbose
|
||||
displayName: Deploy test resources (AzureCloud)
|
||||
condition: and(succeeded(), eq(variables['CloudType'], 'AzureCloud'))
|
||||
|
||||
- pwsh: >
|
||||
eng/common/TestResources/New-TestResources.ps1
|
||||
-BaseName 'Generated'
|
||||
-ServiceDirectory '${{ parameters.ServiceDirectory }}'
|
||||
-TenantId '$(aad-azure-sdk-test-tenant-id-gov)'
|
||||
-TestApplicationId '$(aad-azure-sdk-test-client-id-gov)'
|
||||
-TestApplicationSecret '$(aad-azure-sdk-test-client-secret-gov)'
|
||||
-ProvisionerApplicationId '$(aad-azure-sdk-test-client-id-gov)'
|
||||
-ProvisionerApplicationSecret '$(aad-azure-sdk-test-client-secret-gov)'
|
||||
-AdditionalParameters ${{ parameters.ArmTemplateParameters }}
|
||||
-DeleteAfterHours ${{ parameters.DeleteAfterHours }}
|
||||
-Location '${{ parameters.Location }}'
|
||||
-Environment 'AzureUSGovernment'
|
||||
-CI
|
||||
-Force
|
||||
-Verbose
|
||||
displayName: Deploy test resources (AzureUSGovernment)
|
||||
condition: and(succeeded(), eq(variables['CloudType'], 'AzureUSGovernment'))
|
||||
|
||||
- pwsh: >
|
||||
eng/common/TestResources/New-TestResources.ps1
|
||||
-BaseName 'Generated'
|
||||
-ServiceDirectory '${{ parameters.ServiceDirectory }}'
|
||||
-TenantId '$(aad-azure-sdk-test-tenant-id-cn)'
|
||||
-TestApplicationId '$(aad-azure-sdk-test-client-id-cn)'
|
||||
-TestApplicationSecret '$(aad-azure-sdk-test-client-secret-cn)'
|
||||
-ProvisionerApplicationId '$(aad-azure-sdk-test-client-id-cn)'
|
||||
-ProvisionerApplicationSecret '$(aad-azure-sdk-test-client-secret-cn)'
|
||||
-AdditionalParameters ${{ parameters.ArmTemplateParameters }}
|
||||
-DeleteAfterHours ${{ parameters.DeleteAfterHours }}
|
||||
-Location '${{ parameters.Location }}'
|
||||
-Environment 'AzureChinaCloud'
|
||||
-CI
|
||||
-Force
|
||||
-Verbose
|
||||
displayName: Deploy test resources (AzureChinaCloud)
|
||||
condition: and(succeeded(), eq(variables['CloudType'], 'AzureChinaCloud'))
|
||||
48
eng/common/TestResources/remove-test-resources.yml
Normal file
48
eng/common/TestResources/remove-test-resources.yml
Normal file
@ -0,0 +1,48 @@
|
||||
# Removes resources from a cloud type specified by the variable (not parameter)
|
||||
# 'CloudType'. Use this as part of a matrix to remove resources from a
|
||||
# particular cloud instance. Normally we would use template variables instead of
|
||||
# parameters but matrix variables are not available during template expansion
|
||||
# so any benefits of parameters are lost.
|
||||
|
||||
# Assumes steps in deploy-test-resources.yml was run previously. Requires
|
||||
# environment variable: AZURE_RESOURCEGROUP_NAME and Az PowerShell module
|
||||
|
||||
steps:
|
||||
- pwsh: >
|
||||
eng/common/TestResources/Remove-TestResources.ps1
|
||||
-ResourceGroupName "${env:AZURE_RESOURCEGROUP_NAME}"
|
||||
-TenantId '$(aad-azure-sdk-test-tenant-id)'
|
||||
-ProvisionerApplicationId '$(aad-azure-sdk-test-client-id)'
|
||||
-ProvisionerApplicationSecret '$(aad-azure-sdk-test-client-secret)'
|
||||
-Environment 'AzureCloud'
|
||||
-Force
|
||||
-Verbose
|
||||
displayName: Remove test resources (AzureCloud)
|
||||
condition: and(ne(variables['AZURE_RESOURCEGROUP_NAME'], ''), eq(variables['CloudType'], 'AzureCloud'))
|
||||
continueOnError: true
|
||||
|
||||
- pwsh: >
|
||||
eng/common/TestResources/Remove-TestResources.ps1
|
||||
-ResourceGroupName "${env:AZURE_RESOURCEGROUP_NAME}"
|
||||
-TenantId '$(aad-azure-sdk-test-tenant-id-gov)'
|
||||
-ProvisionerApplicationId '$(aad-azure-sdk-test-client-id-gov)'
|
||||
-ProvisionerApplicationSecret '$(aad-azure-sdk-test-client-secret-gov)'
|
||||
-Environment 'AzureUSGovernment'
|
||||
-Force
|
||||
-Verbose
|
||||
displayName: Remove test resources (AzureUSGovernment)
|
||||
condition: and(ne(variables['AZURE_RESOURCEGROUP_NAME'], ''), eq(variables['CloudType'], 'AzureUSGovernment'))
|
||||
continueOnError: true
|
||||
|
||||
- pwsh: >
|
||||
eng/common/TestResources/Remove-TestResources.ps1
|
||||
-ResourceGroupName "${env:AZURE_RESOURCEGROUP_NAME}"
|
||||
-TenantId '$(aad-azure-sdk-test-tenant-id-cn)'
|
||||
-ProvisionerApplicationId '$(aad-azure-sdk-test-client-id-cn)'
|
||||
-ProvisionerApplicationSecret '$(aad-azure-sdk-test-client-secret-cn)'
|
||||
-Environment 'AzureChinaCloud'
|
||||
-Force
|
||||
-Verbose
|
||||
displayName: Remove test resources (AzureChinaCloud)
|
||||
condition: and(ne(variables['AZURE_RESOURCEGROUP_NAME'], ''), eq(variables['CloudType'], 'AzureChinaCloud'))
|
||||
continueOnError: true
|
||||
135
eng/common/Update-Change-Log.ps1
Normal file
135
eng/common/Update-Change-Log.ps1
Normal file
@ -0,0 +1,135 @@
|
||||
# Note: This script will add or replace version title in change log
|
||||
|
||||
# Parameter description
|
||||
# Version : Version to add or replace in change log
|
||||
# ChangeLogPath: Path to change log file. If change log path is set to directory then script will probe for change log file in that path
|
||||
# Unreleased: Default is true. If it is set to false, then today's date will be set in verion title. If it is True then title will show "Unreleased"
|
||||
# ReplaceVersion: This is useful when replacing current version title with new title.( Helpful to update the title before package release)
|
||||
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[String]$Version,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[String]$ChangeLogPath,
|
||||
[String]$Unreleased = $True,
|
||||
[String]$ReplaceVersion = $False
|
||||
)
|
||||
|
||||
|
||||
$RELEASE_TITLE_REGEX = "(?<releaseNoteTitle>^\#+.*(?<version>\b\d+\.\d+\.\d+([^0-9\s][^\s:]+)?))"
|
||||
$UNRELEASED_TAG = "(Unreleased)"
|
||||
function Version-Matches($line)
|
||||
{
|
||||
return ($line -match $RELEASE_TITLE_REGEX)
|
||||
}
|
||||
|
||||
function Get-ChangelogPath($Path)
|
||||
{
|
||||
# Check if CHANGELOG.md is present in path
|
||||
$ChangeLogPath = Join-Path -Path $Path -ChildPath "CHANGELOG.md"
|
||||
if ((Test-Path -Path $ChangeLogPath) -eq $False){
|
||||
# Check if change log exists with name HISTORY.md
|
||||
$ChangeLogPath = Join-Path -Path $Path -ChildPath "HISTORY.md"
|
||||
if ((Test-Path -Path $ChangeLogPath) -eq $False){
|
||||
Write-Host "Change log is not found in path[$Path]"
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Change log is found at path [$ChangeLogPath]"
|
||||
return $ChangeLogPath
|
||||
}
|
||||
|
||||
|
||||
function Get-VersionTitle($Version, $Unreleased)
|
||||
{
|
||||
# Generate version title
|
||||
$newVersionTitle = "## $Version $UNRELEASED_TAG"
|
||||
if ($Unreleased -eq $False){
|
||||
$releaseDate = Get-Date -Format "(yyyy-MM-dd)"
|
||||
$newVersionTitle = "## $Version $releaseDate"
|
||||
}
|
||||
return $newVersionTitle
|
||||
}
|
||||
|
||||
|
||||
function Get-NewChangeLog( [System.Collections.ArrayList]$ChangelogLines, $Version, $Unreleased, $ReplaceVersion)
|
||||
{
|
||||
|
||||
# version parameter is to pass new version to add or replace
|
||||
# Unreleased parameter can be set to False to set today's date instead of "Unreleased in title"
|
||||
# ReplaceVersion param can be set to true to replace current version title( useful at release time to change title)
|
||||
|
||||
# find index of current version
|
||||
$Index = 0
|
||||
$CurrentTitle = ""
|
||||
for(; $Index -lt $ChangelogLines.Count; $Index++){
|
||||
if (Version-Matches($ChangelogLines[$Index])){
|
||||
$CurrentTitle = $ChangelogLines[$Index]
|
||||
Write-Host "Current Version title: $CurrentTitle"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# Generate version title
|
||||
$newVersionTitle = Get-VersionTitle -Version $Version -Unreleased $Unreleased
|
||||
|
||||
if( $newVersionTitle -eq $CurrentTitle){
|
||||
Write-Host "No change is required in change log. Version is already present."
|
||||
exit(0)
|
||||
}
|
||||
|
||||
# update change log script is triggered for all packages with current version for Java ( or any language where version is maintained in common file)
|
||||
# Do not add new line or replace existing title when version is already present and script is triggered to add new line
|
||||
if (($ReplaceVersion -eq $False) -and ($Unreleased -eq $True) -and $CurrentTitle.Contains($Version)){
|
||||
Write-Host "Version is already present in change log."
|
||||
exit(0)
|
||||
}
|
||||
|
||||
if (($ReplaceVersion -eq $True) -and ($Unreleased -eq $False) -and (-not $CurrentTitle.Contains($UNRELEASED_TAG))){
|
||||
Write-Host "Version is already present in change log with a release date."
|
||||
exit(0)
|
||||
}
|
||||
|
||||
# if current version title already has new version then we should replace title to update it
|
||||
if ($CurrentTitle.Contains($Version) -and $ReplaceVersion -eq $False){
|
||||
Write-Host "Version is already present in title. Updating version title"
|
||||
$ReplaceVersion = $True
|
||||
}
|
||||
|
||||
# if version is already found and not replacing then nothing to do
|
||||
if ($ReplaceVersion -eq $False){
|
||||
Write-Host "Adding version title $newVersionTitle"
|
||||
$ChangelogLines.insert($Index, "")
|
||||
$ChangelogLines.insert($Index, "")
|
||||
$ChangelogLines.insert($Index, $newVersionTitle)
|
||||
}
|
||||
else{
|
||||
# Script is executed to replace an existing version title
|
||||
Write-Host "Replacing current version title to $newVersionTitle"
|
||||
$ChangelogLines[$index] = $newVersionTitle
|
||||
}
|
||||
|
||||
return $ChangelogLines
|
||||
}
|
||||
|
||||
|
||||
# Make sure path is valid
|
||||
if ((Test-Path -Path $ChangeLogPath) -eq $False){
|
||||
Write-Host "Change log path is invalid. [$ChangeLogPath]"
|
||||
exit(1)
|
||||
}
|
||||
|
||||
# probe change log path if path is directory
|
||||
if (Test-Path -Path $ChangeLogPath -PathType Container)
|
||||
{
|
||||
$ChangeLogPath = Get-ChangelogPath -Path $ChangeLogPath
|
||||
}
|
||||
|
||||
# Read current change logs and add/update version
|
||||
$ChangelogLines = [System.Collections.ArrayList](Get-Content -Path $ChangeLogPath)
|
||||
$NewContents = Get-NewChangeLog -ChangelogLines $ChangelogLines -Version $Version -Unreleased $Unreleased -ReplaceVersion $ReplaceVersion
|
||||
|
||||
Write-Host "Writing change log to file [$ChangeLogPath]"
|
||||
Set-Content -Path $ChangeLogPath $NewContents
|
||||
Write-Host "Version is added/updated in change log"
|
||||
60
eng/common/pipelines/templates/steps/create-pull-request.yml
Normal file
60
eng/common/pipelines/templates/steps/create-pull-request.yml
Normal file
@ -0,0 +1,60 @@
|
||||
# Expects azuresdk-github-pat is set to the PAT for azure-sdk
|
||||
# Expects the buildtools to be cloned
|
||||
|
||||
parameters:
|
||||
BaseBranchName: master
|
||||
PRBranchName: not-specified
|
||||
PROwner: azure-sdk
|
||||
CommitMsg: not-specified
|
||||
RepoOwner: Azure
|
||||
RepoName: not-specified
|
||||
PushArgs:
|
||||
WorkingDirectory: $(System.DefaultWorkingDirectory)
|
||||
PRTitle: not-specified
|
||||
|
||||
steps:
|
||||
|
||||
- pwsh: |
|
||||
echo "git add ."
|
||||
git add .
|
||||
|
||||
echo "git diff --name-status --cached --exit-code"
|
||||
git diff --name-status --cached --exit-code
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
echo "##vso[task.setvariable variable=HasChanges]$true"
|
||||
echo "Changes detected so setting HasChanges=true"
|
||||
}
|
||||
else {
|
||||
echo "##vso[task.setvariable variable=HasChanges]$false"
|
||||
echo "No changes so skipping code push"
|
||||
}
|
||||
|
||||
displayName: Check for changes
|
||||
workingDirectory: ${{ parameters.WorkingDirectory }}
|
||||
ignoreLASTEXITCODE: true
|
||||
|
||||
- pwsh: |
|
||||
eng/common/scripts/git-branch-push.ps1 `
|
||||
-PRBranchName "${{ parameters.PRBranchName }}" `
|
||||
-CommitMsg "${{ parameters.CommitMsg }}" `
|
||||
-GitUrl "https://$(azuresdk-github-pat)@github.com/${{ parameters.PROwner }}/${{ parameters.RepoName }}.git" `
|
||||
-PushArgs "${{ parameters.PushArgs }}"
|
||||
|
||||
displayName: Push changes
|
||||
workingDirectory: ${{ parameters.WorkingDirectory }}
|
||||
condition: and(succeeded(), eq(variables['HasChanges'], 'true'))
|
||||
|
||||
- pwsh: |
|
||||
eng/common/scripts/Submit-PullRequest.ps1 `
|
||||
-RepoOwner "${{ parameters.RepoOwner }}" `
|
||||
-RepoName "${{ parameters.RepoName }}" `
|
||||
-BaseBranch "${{ parameters.BaseBranchName }}" `
|
||||
-PROwner "${{ parameters.PROwner }}" `
|
||||
-PRBranch "${{ parameters.PRBranchName }}" `
|
||||
-AuthToken "$(azuresdk-github-pat)" `
|
||||
-PRTitle "${{ parameters.PRTitle }}"
|
||||
|
||||
displayName: Create pull request
|
||||
workingDirectory: ${{ parameters.WorkingDirectory }}
|
||||
condition: and(succeeded(), eq(variables['HasChanges'], 'true'))
|
||||
@ -0,0 +1,18 @@
|
||||
parameters:
|
||||
ArtifactLocation: 'not-specified'
|
||||
PackageRepository: 'not-specified'
|
||||
ReleaseSha: 'not-specified'
|
||||
RepoId: 'not-specified'
|
||||
WorkingDirectory: ''
|
||||
|
||||
steps:
|
||||
- task: PowerShell@2
|
||||
displayName: 'Verify Package Tags and Create Git Releases'
|
||||
inputs:
|
||||
targetType: filePath
|
||||
filePath: eng/common/scripts/create-tags-and-git-release.ps1
|
||||
arguments: -artifactLocation ${{parameters.ArtifactLocation}} -packageRepository ${{parameters.PackageRepository}} -releaseSha ${{parameters.ReleaseSha}} -repoId ${{parameters.RepoId}} -workingDirectory '${{parameters.WorkingDirectory}}'
|
||||
pwsh: true
|
||||
timeoutInMinutes: 5
|
||||
env:
|
||||
GH_TOKEN: $(azuresdk-github-pat)
|
||||
23
eng/common/pipelines/templates/steps/publish-blobs.yml
Normal file
23
eng/common/pipelines/templates/steps/publish-blobs.yml
Normal file
@ -0,0 +1,23 @@
|
||||
parameters:
|
||||
FolderForUpload: ''
|
||||
BlobSASKey: ''
|
||||
TargetLanguage: ''
|
||||
BlobName: ''
|
||||
ScriptPath: ''
|
||||
|
||||
steps:
|
||||
- pwsh: |
|
||||
Invoke-WebRequest -MaximumRetryCount 10 -Uri "https://aka.ms/downloadazcopy-v10-windows" `
|
||||
-OutFile "azcopy.zip" | Wait-Process; Expand-Archive -Path "azcopy.zip" -DestinationPath "$(Build.BinariesDirectory)/azcopy/"
|
||||
workingDirectory: $(Build.BinariesDirectory)
|
||||
displayName: Download and Extract azcopy Zip
|
||||
|
||||
- task: Powershell@2
|
||||
inputs:
|
||||
targetType: 'filePath'
|
||||
filePath: ${{ parameters.ScriptPath }}
|
||||
arguments: -AzCopy $(Resolve-Path "$(Build.BinariesDirectory)/azcopy/azcopy_windows_amd64_*/azcopy.exe")[0] -DocLocation "${{ parameters.FolderForUpload }}" -SASKey "${{ parameters.BlobSASKey }}" -Language "${{ parameters.TargetLanguage }}" -BlobName "${{ parameters.BlobName }}"
|
||||
pwsh: true
|
||||
workingDirectory: $(Pipeline.Workspace)
|
||||
displayName: Copy Docs to Blob
|
||||
continueOnError: false
|
||||
108
eng/common/scripts/Submit-PullRequest.ps1
Normal file
108
eng/common/scripts/Submit-PullRequest.ps1
Normal file
@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env pwsh -c
|
||||
|
||||
<#
|
||||
.DESCRIPTION
|
||||
Creates a GitHub pull request for a given branch if it doesn't already exist
|
||||
.PARAMETER RepoOwner
|
||||
The GitHub repository owner to create the pull request against.
|
||||
.PARAMETER RepoName
|
||||
The GitHub repository name to create the pull request against.
|
||||
.PARAMETER BaseBranch
|
||||
The base or target branch we want the pull request to be against.
|
||||
.PARAMETER PROwner
|
||||
The owner of the branch we want to create a pull request for.
|
||||
.PARAMETER PRBranch
|
||||
The branch which we want to create a pull request for.
|
||||
.PARAMETER AuthToken
|
||||
A personal access token
|
||||
#>
|
||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$RepoOwner,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
$RepoName,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
$BaseBranch,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
$PROwner,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
$PRBranch,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
$AuthToken,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
$PRTitle,
|
||||
$PRBody = $PRTitle
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'stop'
|
||||
Set-StrictMode -Version 1
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
$headers = @{
|
||||
Authorization = "bearer $AuthToken"
|
||||
}
|
||||
|
||||
$query = 'query ($repoOwner: String!, $repoName: String!, $baseRefName: String!) {
|
||||
repository(owner: $repoOwner, name: $repoName) {
|
||||
pullRequests(baseRefName: $baseRefName, states: OPEN, first: 100) {
|
||||
totalCount
|
||||
nodes {
|
||||
number
|
||||
headRef {
|
||||
name
|
||||
repository {
|
||||
name
|
||||
owner {
|
||||
login
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}'
|
||||
|
||||
|
||||
$data = @{
|
||||
query = $query
|
||||
variables = @{
|
||||
repoOwner = $RepoOwner
|
||||
repoName = $RepoName
|
||||
baseRefName = $BaseBranch
|
||||
}
|
||||
}
|
||||
|
||||
$resp = Invoke-RestMethod -Method Post -Headers $headers `
|
||||
https://api.github.com/graphql `
|
||||
-Body ($data | ConvertTo-Json)
|
||||
$resp | Write-Verbose
|
||||
|
||||
$matchingPr = $resp.data.repository.pullRequests.nodes `
|
||||
| ? { $_.headRef.name -eq $PRBranch -and $_.headRef.repository.owner.login -eq $PROwner } `
|
||||
| select -First 1
|
||||
|
||||
if ($matchingPr) {
|
||||
Write-Host -f green "Pull request already exists https://github.com/$RepoOwner/$RepoName/pull/$($matchingPr.number)"
|
||||
}
|
||||
else {
|
||||
$data = @{
|
||||
title = $PRTitle
|
||||
head = "${PROwner}:${PRBranch}"
|
||||
base = $BaseBranch
|
||||
body = $PRBody
|
||||
maintainer_can_modify = $true
|
||||
}
|
||||
|
||||
$resp = Invoke-RestMethod -Method POST -Headers $headers `
|
||||
https://api.github.com/repos/$RepoOwner/$RepoName/pulls `
|
||||
-Body ($data | ConvertTo-Json)
|
||||
$resp | Write-Verbose
|
||||
Write-Host -f green "Pull request created https://github.com/$RepoOwner/$RepoName/pull/$($resp.number)"
|
||||
}
|
||||
326
eng/common/scripts/copy-docs-to-blobstorage.ps1
Normal file
326
eng/common/scripts/copy-docs-to-blobstorage.ps1
Normal file
@ -0,0 +1,326 @@
|
||||
# Note, due to how `Expand-Archive` is leveraged in this script,
|
||||
# powershell core is a requirement for successful execution.
|
||||
param (
|
||||
$AzCopy,
|
||||
$DocLocation,
|
||||
$SASKey,
|
||||
$Language,
|
||||
$BlobName,
|
||||
$ExitOnError=1
|
||||
)
|
||||
$Language = $Language.ToLower()
|
||||
|
||||
# Regex inspired but simplified from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
||||
$SEMVER_REGEX = "^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-?(?<prelabel>[a-zA-Z-]*)(?:\.?(?<prenumber>0|[1-9]\d*)))?$"
|
||||
|
||||
function ToSemVer($version){
|
||||
if ($version -match $SEMVER_REGEX)
|
||||
{
|
||||
if($matches['prelabel'] -eq $null) {
|
||||
# artifically provide these values for non-prereleases to enable easy sorting of them later than prereleases.
|
||||
$prelabel = "zzz"
|
||||
$prenumber = 999;
|
||||
$isPre = $false;
|
||||
}
|
||||
else {
|
||||
$prelabel = $matches["prelabel"]
|
||||
$prenumber = [int]$matches["prenumber"]
|
||||
$isPre = $true;
|
||||
}
|
||||
|
||||
New-Object PSObject -Property @{
|
||||
Major = [int]$matches['major']
|
||||
Minor = [int]$matches['minor']
|
||||
Patch = [int]$matches['patch']
|
||||
PrereleaseLabel = $prelabel
|
||||
PrereleaseNumber = $prenumber
|
||||
IsPrerelease = $isPre
|
||||
RawVersion = $version
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ($ExitOnError)
|
||||
{
|
||||
throw "Unable to convert $version to valid semver and hard exit on error is enabled. Exiting."
|
||||
}
|
||||
else
|
||||
{
|
||||
return $null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function SortSemVersions($versions)
|
||||
{
|
||||
return $versions | Sort -Property Major, Minor, Patch, PrereleaseLabel, PrereleaseNumber -Descending
|
||||
}
|
||||
|
||||
function Sort-Versions
|
||||
{
|
||||
Param (
|
||||
[Parameter(Mandatory=$true)] [string[]]$VersionArray
|
||||
)
|
||||
|
||||
# standard init and sorting existing
|
||||
$versionsObject = New-Object PSObject -Property @{
|
||||
OriginalVersionArray = $VersionArray
|
||||
SortedVersionArray = @()
|
||||
LatestGAPackage = ""
|
||||
RawVersionsList = ""
|
||||
LatestPreviewPackage = ""
|
||||
}
|
||||
|
||||
if ($VersionArray.Count -eq 0)
|
||||
{
|
||||
return $versionsObject
|
||||
}
|
||||
|
||||
$versionsObject.SortedVersionArray = SortSemVersions -versions ($VersionArray | % { ToSemVer $_})
|
||||
$versionsObject.RawVersionsList = $versionsObject.SortedVersionArray | % { $_.RawVersion }
|
||||
|
||||
# handle latest and preview
|
||||
# we only want to hold onto the latest preview if its NEWER than the latest GA.
|
||||
# this means that the latest preview package either A) has to be the latest value in the VersionArray
|
||||
# or B) set to nothing. We'll handle the set to nothing case a bit later.
|
||||
$versionsObject.LatestPreviewPackage = $versionsObject.SortedVersionArray[0].RawVersion
|
||||
$gaVersions = $versionsObject.SortedVersionArray | ? { !$_.IsPrerelease }
|
||||
|
||||
# we have a GA package
|
||||
if ($gaVersions.Count -ne 0)
|
||||
{
|
||||
# GA is the newest non-preview package
|
||||
$versionsObject.LatestGAPackage = $gaVersions[0].RawVersion
|
||||
|
||||
# in the case where latest preview == latestGA (because of our default selection earlier)
|
||||
if ($versionsObject.LatestGAPackage -eq $versionsObject.LatestPreviewPackage)
|
||||
{
|
||||
# latest is newest, unset latest preview
|
||||
$versionsObject.LatestPreviewPackage = ""
|
||||
}
|
||||
}
|
||||
|
||||
return $versionsObject
|
||||
}
|
||||
|
||||
function Get-Existing-Versions
|
||||
{
|
||||
Param (
|
||||
[Parameter(Mandatory=$true)] [String]$PkgName
|
||||
)
|
||||
$versionUri = "$($BlobName)/`$web/$($Language)/$($PkgName)/versioning/versions"
|
||||
Write-Host "Heading to $versionUri to retrieve known versions"
|
||||
|
||||
try {
|
||||
return ((Invoke-RestMethod -Uri $versionUri -MaximumRetryCount 3 -RetryIntervalSec 5) -Split "\n" | % {$_.Trim()} | ? { return $_ })
|
||||
}
|
||||
catch {
|
||||
# Handle 404. If it's 404, this is the first time we've published this package.
|
||||
if ($_.Exception.Response.StatusCode.value__ -eq 404){
|
||||
Write-Host "Version file does not exist. This is the first time we have published this package."
|
||||
}
|
||||
else {
|
||||
# If it's not a 404. exit. We don't know what's gone wrong.
|
||||
Write-Host "Exception getting version file. Aborting"
|
||||
Write-Host $_
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Update-Existing-Versions
|
||||
{
|
||||
Param (
|
||||
[Parameter(Mandatory=$true)] [String]$PkgName,
|
||||
[Parameter(Mandatory=$true)] [String]$PkgVersion,
|
||||
[Parameter(Mandatory=$true)] [String]$DocDest
|
||||
)
|
||||
$existingVersions = @(Get-Existing-Versions -PkgName $PkgName)
|
||||
|
||||
Write-Host "Before I update anything, I am seeing $existingVersions"
|
||||
|
||||
if (!$existingVersions)
|
||||
{
|
||||
$existingVersions = @()
|
||||
$existingVersions += $PkgVersion
|
||||
Write-Host "No existing versions. Adding $PkgVersion."
|
||||
}
|
||||
else
|
||||
{
|
||||
$existingVersions += $pkgVersion
|
||||
Write-Host "Already Existing Versions. Adding $PkgVersion."
|
||||
}
|
||||
|
||||
$existingVersions = $existingVersions | Select-Object -Unique
|
||||
|
||||
# newest first
|
||||
$sortedVersionObj = (Sort-Versions -VersionArray $existingVersions)
|
||||
|
||||
Write-Host $sortedVersionObj
|
||||
Write-Host $sortedVersionObj.LatestGAPackage
|
||||
Write-Host $sortedVersionObj.LatestPreviewPackage
|
||||
|
||||
# write to file. to get the correct performance with "actually empty" files, we gotta do the newline
|
||||
# join ourselves. This way we have absolute control over the trailing whitespace.
|
||||
$sortedVersionObj.RawVersionsList -join "`n" | Out-File -File "$($DocLocation)/versions" -Force -NoNewLine
|
||||
$sortedVersionObj.LatestGAPackage | Out-File -File "$($DocLocation)/latest-ga" -Force -NoNewLine
|
||||
$sortedVersionObj.LatestPreviewPackage | Out-File -File "$($DocLocation)/latest-preview" -Force -NoNewLine
|
||||
|
||||
& $($AzCopy) cp "$($DocLocation)/versions" "$($DocDest)/$($PkgName)/versioning/versions$($SASKey)"
|
||||
& $($AzCopy) cp "$($DocLocation)/latest-preview" "$($DocDest)/$($PkgName)/versioning/latest-preview$($SASKey)"
|
||||
& $($AzCopy) cp "$($DocLocation)/latest-ga" "$($DocDest)/$($PkgName)/versioning/latest-ga$($SASKey)"
|
||||
}
|
||||
|
||||
function Upload-Blobs
|
||||
{
|
||||
Param (
|
||||
[Parameter(Mandatory=$true)] [String]$DocDir,
|
||||
[Parameter(Mandatory=$true)] [String]$PkgName,
|
||||
[Parameter(Mandatory=$true)] [String]$DocVersion
|
||||
)
|
||||
#eg : $BlobName = "https://azuresdkdocs.blob.core.windows.net"
|
||||
$DocDest = "$($BlobName)/`$web/$($Language)"
|
||||
|
||||
Write-Host "DocDest $($DocDest)"
|
||||
Write-Host "PkgName $($PkgName)"
|
||||
Write-Host "DocVersion $($DocVersion)"
|
||||
Write-Host "DocDir $($DocDir)"
|
||||
Write-Host "Final Dest $($DocDest)/$($PkgName)/$($DocVersion)"
|
||||
|
||||
Write-Host "Uploading $($PkgName)/$($DocVersion) to $($DocDest)..."
|
||||
& $($AzCopy) cp "$($DocDir)/**" "$($DocDest)/$($PkgName)/$($DocVersion)$($SASKey)" --recursive=true
|
||||
|
||||
Write-Host "Handling versioning files under $($DocDest)/$($PkgName)/versioning/"
|
||||
Update-Existing-Versions -PkgName $PkgName -PkgVersion $DocVersion -DocDest $DocDest
|
||||
}
|
||||
|
||||
|
||||
if ($Language -eq "javascript")
|
||||
{
|
||||
$PublishedDocs = Get-ChildItem "$($DocLocation)/documentation" | Where-Object -FilterScript {$_.Name.EndsWith(".zip")}
|
||||
|
||||
foreach ($Item in $PublishedDocs) {
|
||||
$PkgName = "azure-$($Item.BaseName)"
|
||||
Write-Host $PkgName
|
||||
Expand-Archive -Force -Path "$($DocLocation)/documentation/$($Item.Name)" -DestinationPath "$($DocLocation)/documentation/$($Item.BaseName)"
|
||||
$dirList = Get-ChildItem "$($DocLocation)/documentation/$($Item.BaseName)/$($Item.BaseName)" -Attributes Directory
|
||||
|
||||
if($dirList.Length -eq 1){
|
||||
$DocVersion = $dirList[0].Name
|
||||
Write-Host "Uploading Doc for $($PkgName) Version:- $($DocVersion)..."
|
||||
Upload-Blobs -DocDir "$($DocLocation)/documentation/$($Item.BaseName)/$($Item.BaseName)/$($DocVersion)" -PkgName $PkgName -DocVersion $DocVersion
|
||||
}
|
||||
else{
|
||||
Write-Host "found more than 1 folder under the documentation for package - $($Item.Name)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($Language -eq "dotnet")
|
||||
{
|
||||
$PublishedPkgs = Get-ChildItem "$($DocLocation)/packages" | Where-Object -FilterScript {$_.Name.EndsWith(".nupkg") -and -not $_.Name.EndsWith(".symbols.nupkg")}
|
||||
$PublishedDocs = Get-ChildItem "$($DocLocation)" | Where-Object -FilterScript {$_.Name.StartsWith("Docs.")}
|
||||
|
||||
foreach ($Item in $PublishedDocs) {
|
||||
$PkgName = $Item.Name.Remove(0, 5)
|
||||
$PkgFullName = $PublishedPkgs | Where-Object -FilterScript {$_.Name -match "$($PkgName).\d"}
|
||||
|
||||
if (($PkgFullName | Measure-Object).count -eq 1)
|
||||
{
|
||||
$DocVersion = $PkgFullName[0].BaseName.Remove(0, $PkgName.Length + 1)
|
||||
|
||||
Write-Host "Start Upload for $($PkgName)/$($DocVersion)"
|
||||
Write-Host "DocDir $($Item)"
|
||||
Write-Host "PkgName $($PkgName)"
|
||||
Write-Host "DocVersion $($DocVersion)"
|
||||
Upload-Blobs -DocDir "$($Item)" -PkgName $PkgName -DocVersion $DocVersion
|
||||
}
|
||||
else
|
||||
{
|
||||
Write-Host "Package with the same name Exists. Upload Skipped"
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($Language -eq "python")
|
||||
{
|
||||
$PublishedDocs = Get-ChildItem "$DocLocation" | Where-Object -FilterScript {$_.Name.EndsWith(".zip")}
|
||||
|
||||
foreach ($Item in $PublishedDocs) {
|
||||
$PkgName = $Item.BaseName
|
||||
$ZippedDocumentationPath = Join-Path -Path $DocLocation -ChildPath $Item.Name
|
||||
$UnzippedDocumentationPath = Join-Path -Path $DocLocation -ChildPath $PkgName
|
||||
$VersionFileLocation = Join-Path -Path $UnzippedDocumentationPath -ChildPath "version.txt"
|
||||
|
||||
Expand-Archive -Force -Path $ZippedDocumentationPath -DestinationPath $UnzippedDocumentationPath
|
||||
|
||||
$Version = $(Get-Content $VersionFileLocation).Trim()
|
||||
|
||||
Write-Host "Discovered Package Name: $PkgName"
|
||||
Write-Host "Discovered Package Version: $Version"
|
||||
Write-Host "Directory for Upload: $UnzippedDocumentationPath"
|
||||
|
||||
Upload-Blobs -DocDir $UnzippedDocumentationPath -PkgName $PkgName -DocVersion $Version
|
||||
}
|
||||
}
|
||||
|
||||
if ($Language -eq "java")
|
||||
{
|
||||
$PublishedDocs = Get-ChildItem "$DocLocation" | Where-Object -FilterScript {$_.Name.EndsWith("-javadoc.jar")}
|
||||
foreach ($Item in $PublishedDocs) {
|
||||
$UnjarredDocumentationPath = ""
|
||||
try {
|
||||
$PkgName = $Item.BaseName
|
||||
# The jar's unpacking command doesn't allow specifying a target directory
|
||||
# and will unjar all of the files in whatever the current directory is.
|
||||
# Create a subdirectory to unjar into, set the location, unjar and then
|
||||
# set the location back to its original location.
|
||||
$UnjarredDocumentationPath = Join-Path -Path $DocLocation -ChildPath $PkgName
|
||||
New-Item -ItemType directory -Path "$UnjarredDocumentationPath"
|
||||
$CurrentLocation = Get-Location
|
||||
Set-Location $UnjarredDocumentationPath
|
||||
jar -xf "$($Item.FullName)"
|
||||
Set-Location $CurrentLocation
|
||||
|
||||
# Get the POM file for the artifact we're processing
|
||||
$PomFile = $Item.FullName.Substring(0,$Item.FullName.LastIndexOf(("-javadoc.jar"))) + ".pom"
|
||||
Write-Host "PomFile $($PomFile)"
|
||||
|
||||
# Pull the version from the POM
|
||||
[xml]$PomXml = Get-Content $PomFile
|
||||
$Version = $PomXml.project.version
|
||||
$ArtifactId = $PomXml.project.artifactId
|
||||
|
||||
Write-Host "Start Upload for $($PkgName)/$($Version)"
|
||||
Write-Host "DocDir $($UnjarredDocumentationPath)"
|
||||
Write-Host "PkgName $($ArtifactId)"
|
||||
Write-Host "DocVersion $($Version)"
|
||||
|
||||
Upload-Blobs -DocDir $UnjarredDocumentationPath -PkgName $ArtifactId -DocVersion $Version
|
||||
|
||||
} Finally {
|
||||
if (![string]::IsNullOrEmpty($UnjarredDocumentationPath)) {
|
||||
if (Test-Path -Path $UnjarredDocumentationPath) {
|
||||
Write-Host "Cleaning up $UnjarredDocumentationPath"
|
||||
Remove-Item -Recurse -Force $UnjarredDocumentationPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($Language -eq "c")
|
||||
{
|
||||
# The documentation publishing process for C differs from the other
|
||||
# langauges in this file because this script is invoked once per library
|
||||
# publishing. It is not, for example, invoked once per service publishing.
|
||||
# This is also the case for other langauge publishing steps above... Those
|
||||
# loops are left over from previous versions of this script which were used
|
||||
# to publish multiple docs packages in a single invocation.
|
||||
$pkgInfo = Get-Content $DocLocation/package-info.json | ConvertFrom-Json
|
||||
$pkgName = $pkgInfo.name
|
||||
$pkgVersion = $pkgInfo.version
|
||||
|
||||
Upload-Blobs -DocDir $DocLocation -PkgName $pkgName -DocVersion $pkgVersion
|
||||
}
|
||||
481
eng/common/scripts/create-tags-and-git-release.ps1
Normal file
481
eng/common/scripts/create-tags-and-git-release.ps1
Normal file
@ -0,0 +1,481 @@
|
||||
# ASSUMPTIONS
|
||||
# * that `npm` cli is present for querying available npm packages
|
||||
# * that an environment variable $env:GH_TOKEN is populated with the appropriate PAT to allow pushing of github releases
|
||||
|
||||
param (
|
||||
# used by VerifyPackages
|
||||
$artifactLocation, # the root of the artifact folder. DevOps $(System.ArtifactsDirectory)
|
||||
$workingDirectory, # directory that package artifacts will be extracted into for examination (if necessary)
|
||||
$packageRepository, # used to indicate destination against which we will check the existing version.
|
||||
# valid options: PyPI, Nuget, NPM, Maven, C
|
||||
# used by CreateTags
|
||||
$releaseSha, # the SHA for the artifacts. DevOps: $(Release.Artifacts.<artifactAlias>.SourceVersion) or $(Build.SourceVersion)
|
||||
|
||||
# used by Git Release
|
||||
$repoOwner = "", # the owning organization of the repository. EG "Azure"
|
||||
$repoName = "", # the name of the repository. EG "azure-sdk-for-java"
|
||||
$repoId = "$repoOwner/$repoName", # full repo id. EG azure/azure-sdk-for-net DevOps: $(Build.Repository.Id),
|
||||
[switch]$forceCreate = $false
|
||||
)
|
||||
|
||||
$VERSION_REGEX = "(?<major>\d+)(\.(?<minor>\d+))?(\.(?<patch>\d+))?((?<pre>[^0-9][^\s]+))?"
|
||||
$SDIST_PACKAGE_REGEX = "^(?<package>.*)\-(?<versionstring>$VERSION_REGEX$)"
|
||||
|
||||
# Posts a github release for each item of the pkgList variable. SilentlyContinue
|
||||
function CreateReleases($pkgList, $releaseApiUrl, $releaseSha) {
|
||||
foreach ($pkgInfo in $pkgList) {
|
||||
Write-Host "Creating release $($pkgInfo.Tag)"
|
||||
|
||||
$releaseNotes = ""
|
||||
if ($pkgInfo.ReleaseNotes[$pkgInfo.PackageVersion].ReleaseContent -ne $null) {
|
||||
$releaseNotes = $pkgInfo.ReleaseNotes[$pkgInfo.PackageVersion].ReleaseContent
|
||||
}
|
||||
|
||||
$isPrerelease = $False
|
||||
if ($pkgInfo.PackageVersion -match $VERSION_REGEX) {
|
||||
$preReleaseLabel = $matches["pre"]
|
||||
$isPrerelease = ![string]::IsNullOrEmpty($preReleaseLabel)
|
||||
}
|
||||
|
||||
$url = $releaseApiUrl
|
||||
$body = ConvertTo-Json @{
|
||||
tag_name = $pkgInfo.Tag
|
||||
target_commitish = $releaseSha
|
||||
name = $pkgInfo.Tag
|
||||
draft = $False
|
||||
prerelease = $isPrerelease
|
||||
body = $releaseNotes
|
||||
}
|
||||
|
||||
$headers = @{
|
||||
"Content-Type" = "application/json"
|
||||
"Authorization" = "token $($env:GH_TOKEN)"
|
||||
}
|
||||
|
||||
FireAPIRequest -url $url -body $body -headers $headers -method "Post"
|
||||
}
|
||||
}
|
||||
|
||||
function FireAPIRequest($url, $method, $body = $null, $headers = $null) {
|
||||
$attempts = 1
|
||||
|
||||
while ($attempts -le 3) {
|
||||
try {
|
||||
return Invoke-RestMethod -Method $method -Uri $url -Body $body -Headers $headers
|
||||
}
|
||||
catch {
|
||||
$response = $_.Exception.Response
|
||||
|
||||
$statusCode = $response.StatusCode.value__
|
||||
$statusDescription = $response.StatusDescription
|
||||
|
||||
if ($statusCode) {
|
||||
Write-Host "API request attempt number $attempts to $url failed with statuscode $statusCode"
|
||||
Write-Host $statusDescription
|
||||
|
||||
Write-Host "Rate Limit Details:"
|
||||
Write-Host "Total: $($response.Headers.GetValues("X-RateLimit-Limit"))"
|
||||
Write-Host "Remaining: $($response.Headers.GetValues("X-RateLimit-Remaining"))"
|
||||
Write-Host "Reset Epoch: $($response.Headers.GetValues("X-RateLimit-Reset"))"
|
||||
}
|
||||
else {
|
||||
Write-Host "API request attempt number $attempts to $url failed with no statuscode present, exception follows:"
|
||||
Write-Host $_.Exception.Response
|
||||
Write-Host $_.Exception
|
||||
}
|
||||
|
||||
if ($attempts -ge 3) {
|
||||
Write-Host "Abandoning Request $url after 3 attempts."
|
||||
exit(1)
|
||||
}
|
||||
|
||||
Start-Sleep -s 10
|
||||
}
|
||||
|
||||
$attempts += 1
|
||||
}
|
||||
}
|
||||
|
||||
# Parse out package publishing information given a maven POM file
|
||||
function ParseMavenPackage($pkg, $workingDirectory) {
|
||||
[xml]$contentXML = Get-Content $pkg
|
||||
|
||||
$pkgId = $contentXML.project.artifactId
|
||||
$pkgVersion = $contentXML.project.version
|
||||
$groupId = if ($contentXML.project.groupId -eq $null) { $contentXML.project.parent.groupId } else { $contentXML.project.groupId }
|
||||
|
||||
# if it's a snapshot. return $null (as we don't want to create tags for this, but we also don't want to fail)
|
||||
if ($pkgVersion.Contains("SNAPSHOT")) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$releaseNotes = &"${PSScriptRoot}/../Extract-ReleaseNotes.ps1" -ChangeLogLocation @(Get-ChildItem -Path $pkg.DirectoryName -Recurse -Include "$($pkg.Basename)-changelog.md")[0]
|
||||
|
||||
return New-Object PSObject -Property @{
|
||||
PackageId = $pkgId
|
||||
PackageVersion = $pkgVersion
|
||||
Deployable = $forceCreate -or !(IsMavenPackageVersionPublished -pkgId $pkgId -pkgVersion $pkgVersion -groupId $groupId.Replace(".", "/"))
|
||||
ReleaseNotes = $releaseNotes
|
||||
}
|
||||
}
|
||||
|
||||
# Returns the maven (really sonatype) publish status of a package id and version.
|
||||
function IsMavenPackageVersionPublished($pkgId, $pkgVersion, $groupId) {
|
||||
try {
|
||||
|
||||
$uri = "https://oss.sonatype.org/content/repositories/releases/$groupId/$pkgId/$pkgVersion/$pkgId-$pkgVersion.pom"
|
||||
$pomContent = Invoke-RestMethod -Method "GET" -Uri $uri
|
||||
|
||||
if ($pomContent -ne $null -or $pomContent.Length -eq 0) {
|
||||
return $true
|
||||
}
|
||||
else {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$statusCode = $_.Exception.Response.StatusCode.value__
|
||||
$statusDescription = $_.Exception.Response.StatusDescription
|
||||
|
||||
# if this is 404ing, then this pkg has never been published before
|
||||
if ($statusCode -eq 404) {
|
||||
return $false
|
||||
}
|
||||
|
||||
Write-Host "VersionCheck to maven for packageId $pkgId failed with statuscode $statusCode"
|
||||
Write-Host $statusDescription
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
# make certain to always take the package json closest to the top
|
||||
function ResolvePkgJson($workFolder) {
|
||||
$pathsWithComplexity = @()
|
||||
foreach ($file in (Get-ChildItem -Path $workFolder -Recurse -Include "package.json")) {
|
||||
$complexity = ($file.FullName -Split { $_ -eq "/" -or $_ -eq "\" }).Length
|
||||
$pathsWithComplexity += New-Object PSObject -Property @{
|
||||
Path = $file
|
||||
Complexity = $complexity
|
||||
}
|
||||
}
|
||||
|
||||
return ($pathsWithComplexity | Sort-Object -Property Complexity)[0].Path
|
||||
}
|
||||
|
||||
# Parse out package publishing information given a .tgz npm artifact
|
||||
function ParseNPMPackage($pkg, $workingDirectory) {
|
||||
$workFolder = "$workingDirectory$($pkg.Basename)"
|
||||
$origFolder = Get-Location
|
||||
mkdir $workFolder
|
||||
cd $workFolder
|
||||
|
||||
tar -xzf $pkg
|
||||
|
||||
$packageJSON = ResolvePkgJson -workFolder $workFolder | Get-Content | ConvertFrom-Json
|
||||
$releaseNotes = &"${PSScriptRoot}/../Extract-ReleaseNotes.ps1" -ChangeLogLocation @(Get-ChildItem -Path $workFolder -Recurse -Include "CHANGELOG.md")[0]
|
||||
|
||||
cd $origFolder
|
||||
Remove-Item $workFolder -Force -Recurse -ErrorAction SilentlyContinue
|
||||
|
||||
$pkgId = $packageJSON.name
|
||||
$pkgVersion = $packageJSON.version
|
||||
|
||||
$resultObj = New-Object PSObject -Property @{
|
||||
PackageId = $pkgId
|
||||
PackageVersion = $pkgVersion
|
||||
Deployable = $forceCreate -or !(IsNPMPackageVersionPublished -pkgId $pkgId -pkgVersion $pkgVersion)
|
||||
ReleaseNotes = $releaseNotes
|
||||
}
|
||||
|
||||
return $resultObj
|
||||
}
|
||||
|
||||
# Returns the npm publish status of a package id and version.
|
||||
function IsNPMPackageVersionPublished($pkgId, $pkgVersion) {
|
||||
$npmVersions = (npm show $pkgId versions)
|
||||
|
||||
if ($LastExitCode -ne 0) {
|
||||
npm ping
|
||||
|
||||
if ($LastExitCode -eq 0) {
|
||||
return $False
|
||||
}
|
||||
|
||||
Write-Host "Could not find a deployed version of $pkgId, and NPM connectivity check failed."
|
||||
exit(1)
|
||||
}
|
||||
|
||||
$npmVersionList = $npmVersions.split(",") | % { return $_.replace("[", "").replace("]", "").Trim() }
|
||||
return $npmVersionList.Contains($pkgVersion)
|
||||
}
|
||||
|
||||
# Parse out package publishing information given a nupkg ZIP format.
|
||||
function ParseNugetPackage($pkg, $workingDirectory) {
|
||||
$workFolder = "$workingDirectory$($pkg.Basename)"
|
||||
$origFolder = Get-Location
|
||||
$zipFileLocation = "$workFolder/$($pkg.Basename).zip"
|
||||
mkdir $workFolder
|
||||
|
||||
Copy-Item -Path $pkg -Destination $zipFileLocation
|
||||
Expand-Archive -Path $zipFileLocation -DestinationPath $workFolder
|
||||
[xml] $packageXML = Get-ChildItem -Path "$workFolder/*.nuspec" | Get-Content
|
||||
$releaseNotes = &"${PSScriptRoot}/../Extract-ReleaseNotes.ps1" -ChangeLogLocation @(Get-ChildItem -Path $workFolder -Recurse -Include "CHANGELOG.md")[0]
|
||||
|
||||
Remove-Item $workFolder -Force -Recurse -ErrorAction SilentlyContinue
|
||||
$pkgId = $packageXML.package.metadata.id
|
||||
$pkgVersion = $packageXML.package.metadata.version
|
||||
|
||||
return New-Object PSObject -Property @{
|
||||
PackageId = $pkgId
|
||||
PackageVersion = $pkgVersion
|
||||
Deployable = $forceCreate -or !(IsNugetPackageVersionPublished -pkgId $pkgId -pkgVersion $pkgVersion)
|
||||
ReleaseNotes = $releaseNotes
|
||||
}
|
||||
}
|
||||
|
||||
# Returns the nuget publish status of a package id and version.
|
||||
function IsNugetPackageVersionPublished($pkgId, $pkgVersion) {
|
||||
|
||||
$nugetUri = "https://api.nuget.org/v3-flatcontainer/$($pkgId.ToLowerInvariant())/index.json"
|
||||
|
||||
try {
|
||||
$nugetVersions = Invoke-RestMethod -Method "GET" -Uri $nugetUri
|
||||
|
||||
return $nugetVersions.versions.Contains($pkgVersion)
|
||||
}
|
||||
catch {
|
||||
$statusCode = $_.Exception.Response.StatusCode.value__
|
||||
$statusDescription = $_.Exception.Response.StatusDescription
|
||||
|
||||
# if this is 404ing, then this pkg has never been published before
|
||||
if ($statusCode -eq 404) {
|
||||
return $False
|
||||
}
|
||||
|
||||
Write-Host "Nuget Invocation failed:"
|
||||
Write-Host "StatusCode:" $statusCode
|
||||
Write-Host "StatusDescription:" $statusDescription
|
||||
exit(1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
# Parse out package publishing information given a python sdist of ZIP format.
|
||||
function ParsePyPIPackage($pkg, $workingDirectory) {
|
||||
$pkg.Basename -match $SDIST_PACKAGE_REGEX | Out-Null
|
||||
|
||||
$pkgId = $matches["package"]
|
||||
$pkgVersion = $matches["versionstring"]
|
||||
|
||||
$workFolder = "$workingDirectory$($pkg.Basename)"
|
||||
$origFolder = Get-Location
|
||||
mkdir $workFolder
|
||||
|
||||
Expand-Archive -Path $pkg -DestinationPath $workFolder
|
||||
$releaseNotes = &"${PSScriptRoot}/../Extract-ReleaseNotes.ps1" -ChangeLogLocation @(Get-ChildItem -Path $workFolder -Recurse -Include "CHANGELOG.md")[0]
|
||||
Remove-Item $workFolder -Force -Recurse -ErrorAction SilentlyContinue
|
||||
|
||||
return New-Object PSObject -Property @{
|
||||
PackageId = $pkgId
|
||||
PackageVersion = $pkgVersion
|
||||
Deployable = $forceCreate -or !(IsPythonPackageVersionPublished -pkgId $pkgId -pkgVersion $pkgVersion)
|
||||
ReleaseNotes = $releaseNotes
|
||||
}
|
||||
}
|
||||
|
||||
function ParseCArtifact($pkg, $workingDirectory) {
|
||||
$packageInfo = Get-Content -Raw -Path $pkg | ConvertFrom-JSON
|
||||
$packageArtifactLocation = (Get-ItemProperty $pkg).Directory.FullName
|
||||
|
||||
$releaseNotes = ExtractReleaseNotes -changeLogLocation @(Get-ChildItem -Path $packageArtifactLocation -Recurse -Include "CHANGELOG.md")[0]
|
||||
|
||||
return New-Object PSObject -Property @{
|
||||
PackageId = $packageInfo.name
|
||||
PackageVersion = $packageInfo.version
|
||||
# Artifact info is always considered deployable for C becasue it is not
|
||||
# deployed anywhere. Dealing with duplicate tags happens downstream in
|
||||
# CheckArtifactShaAgainstTagsList
|
||||
Deployable = $true
|
||||
ReleaseNotes = $releaseNotes
|
||||
}
|
||||
}
|
||||
|
||||
# Returns the pypi publish status of a package id and version.
|
||||
function IsPythonPackageVersionPublished($pkgId, $pkgVersion) {
|
||||
try {
|
||||
$existingVersion = (Invoke-RestMethod -Method "Get" -Uri "https://pypi.org/pypi/$pkgId/$pkgVersion/json").info.version
|
||||
|
||||
# if existingVersion exists, then it's already been published
|
||||
return $True
|
||||
}
|
||||
catch {
|
||||
$statusCode = $_.Exception.Response.StatusCode.value__
|
||||
$statusDescription = $_.Exception.Response.StatusDescription
|
||||
|
||||
# if this is 404ing, then this pkg has never been published before
|
||||
if ($statusCode -eq 404) {
|
||||
return $False
|
||||
}
|
||||
|
||||
Write-Host "PyPI Invocation failed:"
|
||||
Write-Host "StatusCode:" $statusCode
|
||||
Write-Host "StatusDescription:" $statusDescription
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
# Retrieves the list of all tags that exist on the target repository
|
||||
function GetExistingTags($apiUrl) {
|
||||
try {
|
||||
return (Invoke-RestMethod -Method "GET" -Uri "$apiUrl/git/refs/tags" ) | % { $_.ref.Replace("refs/tags/", "") }
|
||||
}
|
||||
catch {
|
||||
$statusCode = $_.Exception.Response.StatusCode.value__
|
||||
$statusDescription = $_.Exception.Response.StatusDescription
|
||||
|
||||
Write-Host "Failed to retrieve tags from repository."
|
||||
Write-Host "StatusCode:" $statusCode
|
||||
Write-Host "StatusDescription:" $statusDescription
|
||||
|
||||
# Return an empty list if there are no tags in the repo
|
||||
if ($statusCode -eq 404) {
|
||||
return @()
|
||||
}
|
||||
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
# Walk across all build artifacts, check them against the appropriate repository, return a list of tags/releases
|
||||
function VerifyPackages($pkgRepository, $artifactLocation, $workingDirectory, $apiUrl, $releaseSha) {
|
||||
$pkgList = [array]@()
|
||||
$ParsePkgInfoFn = ""
|
||||
$packagePattern = ""
|
||||
|
||||
switch ($pkgRepository) {
|
||||
"Maven" {
|
||||
$ParsePkgInfoFn = "ParseMavenPackage"
|
||||
$packagePattern = "*.pom"
|
||||
break
|
||||
}
|
||||
"Nuget" {
|
||||
$ParsePkgInfoFn = "ParseNugetPackage"
|
||||
$packagePattern = "*.nupkg"
|
||||
break
|
||||
}
|
||||
"NPM" {
|
||||
$ParsePkgInfoFn = "ParseNPMPackage"
|
||||
$packagePattern = "*.tgz"
|
||||
break
|
||||
}
|
||||
"PyPI" {
|
||||
$ParsePkgInfoFn = "ParsePyPIPackage"
|
||||
$packagePattern = "*.zip"
|
||||
break
|
||||
}
|
||||
"C" {
|
||||
$ParsePkgInfoFn = "ParseCArtifact"
|
||||
$packagePattern = "*.json"
|
||||
}
|
||||
default {
|
||||
Write-Host "Unrecognized Language: $language"
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
$pkgs = (Get-ChildItem -Path $artifactLocation -Include $packagePattern -Recurse -File)
|
||||
|
||||
Write-Host $pkgs
|
||||
|
||||
foreach ($pkg in $pkgs) {
|
||||
try {
|
||||
$parsedPackage = &$ParsePkgInfoFn -pkg $pkg -workingDirectory $workingDirectory
|
||||
|
||||
if ($parsedPackage -eq $null) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ($parsedPackage.Deployable -ne $True) {
|
||||
Write-Host "Package $($parsedPackage.PackageId) is marked with version $($parsedPackage.PackageVersion), the version $($parsedPackage.PackageVersion) has already been deployed to the target repository."
|
||||
Write-Host "Maybe a pkg version wasn't updated properly?"
|
||||
exit(1)
|
||||
}
|
||||
|
||||
$pkgList += New-Object PSObject -Property @{
|
||||
PackageId = $parsedPackage.PackageId
|
||||
PackageVersion = $parsedPackage.PackageVersion
|
||||
Tag = ($parsedPackage.PackageId + "_" + $parsedPackage.PackageVersion)
|
||||
ReleaseNotes = $parsedPackage.ReleaseNotes
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host $_.Exception.Message
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
$results = ([array]$pkgList | Sort-Object -Property Tag -uniq)
|
||||
|
||||
$existingTags = GetExistingTags($apiUrl)
|
||||
$intersect = $results | % { $_.Tag } | ? { $existingTags -contains $_ }
|
||||
|
||||
if ($intersect.Length -gt 0) {
|
||||
CheckArtifactShaAgainstTagsList -priorExistingTagList $intersect -releaseSha $releaseSha -apiUrl $apiUrl
|
||||
|
||||
# all the tags are clean. remove them from the list of releases we will publish.
|
||||
$results = $results | ? { -not ($intersect -contains $_.Tag ) }
|
||||
}
|
||||
|
||||
return $results
|
||||
}
|
||||
|
||||
# given a set of tags that we want to release, we need to ensure that if they already DO exist.
|
||||
# if they DO exist, quietly exit if the commit sha of the artifact matches that of the tag
|
||||
# if the commit sha does not match, exit with error and report both problem shas
|
||||
function CheckArtifactShaAgainstTagsList($priorExistingTagList, $releaseSha, $apiUrl) {
|
||||
$headers = @{
|
||||
"Content-Type" = "application/json"
|
||||
"Authorization" = "token $($env:GH_TOKEN)"
|
||||
}
|
||||
|
||||
$unmatchedTags = @()
|
||||
|
||||
foreach ($tag in $priorExistingTagList) {
|
||||
$tagSha = (FireAPIRequest -Method "Get" -Url "$apiUrl/git/refs/tags/$tag" -Headers $headers)."object".sha
|
||||
|
||||
if ($tagSha -eq $releaseSha) {
|
||||
Write-Host "This package has already been released. The existing tag commit SHA $releaseSha matches the artifact SHA being processed. Skipping release step for this tag."
|
||||
}
|
||||
else {
|
||||
Write-Host "The artifact SHA $releaseSha does not match that of the currently existing tag."
|
||||
Write-Host "Tag with issues is $tag with commit SHA $tagSha"
|
||||
|
||||
$unmatchedTags += $tag
|
||||
}
|
||||
}
|
||||
|
||||
if ($unmatchedTags.Length -gt 0) {
|
||||
Write-Host "Tags already existing with different SHA versions. Exiting."
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
$apiUrl = "https://api.github.com/repos/$repoId"
|
||||
Write-Host "Using API URL $apiUrl"
|
||||
|
||||
# VERIFY PACKAGES
|
||||
$pkgList = VerifyPackages -pkgRepository $packageRepository -artifactLocation $artifactLocation -workingDirectory $workingDirectory -apiUrl $apiUrl -releaseSha $releaseSha
|
||||
|
||||
if ($pkgList) {
|
||||
Write-Host "Given the visible artifacts, github releases will be created for the following:"
|
||||
|
||||
foreach ($packageInfo in $pkgList) {
|
||||
Write-Host $packageInfo.Tag
|
||||
}
|
||||
|
||||
# CREATE TAGS and RELEASES
|
||||
CreateReleases -pkgList $pkgList -releaseApiUrl $apiUrl/releases -releaseSha $releaseSha
|
||||
}
|
||||
else {
|
||||
Write-Host "After processing, no packages required release."
|
||||
}
|
||||
145
eng/common/scripts/git-branch-push.ps1
Normal file
145
eng/common/scripts/git-branch-push.ps1
Normal file
@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env pwsh -c
|
||||
|
||||
<#
|
||||
.DESCRIPTION
|
||||
Create local branch of the given repo and attempt to push changes. The push may fail if
|
||||
there has been other changes pushed to the same branch, if so, fetch, rebase and try again.
|
||||
.PARAMETER PRBranchName
|
||||
The name of the github branch the changes are being put into
|
||||
.PARAMETER CommitMsg
|
||||
The message for this particular commit
|
||||
.PARAMETER GitUrl
|
||||
The GitHub repository URL
|
||||
.PARAMETER PushArgs
|
||||
Optional arguments to the push command
|
||||
#>
|
||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $PRBranchName,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $CommitMsg,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $GitUrl,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string] $PushArgs = ""
|
||||
)
|
||||
|
||||
# This is necessay because of the janky git command output writing to stderr.
|
||||
# Without explicitly setting the ErrorActionPreference to continue the script
|
||||
# would fail the first time git wrote command output.
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
Write-Host "git remote add azure-sdk-fork $GitUrl"
|
||||
git remote add azure-sdk-fork $GitUrl
|
||||
if ($LASTEXITCODE -ne 0)
|
||||
{
|
||||
Write-Error "Unable to add remote LASTEXITCODE=$($LASTEXITCODE), see command output above."
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host "git fetch azure-sdk-fork"
|
||||
git fetch azure-sdk-fork
|
||||
if ($LASTEXITCODE -ne 0)
|
||||
{
|
||||
Write-Error "Unable to fetch remote LASTEXITCODE=$($LASTEXITCODE), see command output above."
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host "git checkout -b $PRBranchName"
|
||||
git checkout -b $PRBranchName
|
||||
if ($LASTEXITCODE -ne 0)
|
||||
{
|
||||
Write-Error "Unable to create branch LASTEXITCODE=$($LASTEXITCODE), see command output above."
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host "git -c user.name=`"azure-sdk`" -c user.email=`"azuresdk@microsoft.com`" commit -am `"$($CommitMsg)`""
|
||||
git -c user.name="azure-sdk" -c user.email="azuresdk@microsoft.com" commit -am "$($CommitMsg)"
|
||||
if ($LASTEXITCODE -ne 0)
|
||||
{
|
||||
Write-Error "Unable to add files and create commit LASTEXITCODE=$($LASTEXITCODE), see command output above."
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
# The number of retries can be increased if necessary. In theory, the number of retries
|
||||
# should be the max number of libraries in the largest pipeline -1 as everything except
|
||||
# the first commit could hit issues and need to rebase. The reason this isn't set to that
|
||||
# is because the largest pipeline is cognitive services which has 18 libraries in its
|
||||
# pipeline and that just seemed a bit too large and 10 seemed like a good starting value.
|
||||
$numberOfRetries = 10
|
||||
$needsRetry = $false
|
||||
$tryNumber = 0
|
||||
do
|
||||
{
|
||||
$needsRetry = $false
|
||||
Write-Host "git push azure-sdk-fork $PRBranchName $PushArgs"
|
||||
git push azure-sdk-fork $PRBranchName $PushArgs
|
||||
$tryNumber++
|
||||
if ($LASTEXITCODE -ne 0)
|
||||
{
|
||||
$needsRetry = $true
|
||||
Write-Host "Git push failed with LASTEXITCODE=$($LASTEXITCODE) Need to fetch and rebase: attempt number=$($tryNumber)"
|
||||
Write-Host "git fetch azure-sdk-fork"
|
||||
git fetch azure-sdk-fork
|
||||
if ($LASTEXITCODE -ne 0)
|
||||
{
|
||||
Write-Error "Unable to fetch remote LASTEXITCODE=$($LASTEXITCODE), see command output above."
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$TempPatchFile = New-TemporaryFile
|
||||
Write-Host "git diff ${PRBranchName}~ ${PRBranchName} --output $TempPatchFile"
|
||||
git diff ${PRBranchName}~ ${PRBranchName} --output $TempPatchFile
|
||||
if ($LASTEXITCODE -ne 0)
|
||||
{
|
||||
Write-Error "Unable to create diff file LASTEXITCODE=$($LASTEXITCODE), see command output above."
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Host "git reset --hard azure-sdk-fork/${PRBranchName}"
|
||||
git reset --hard azure-sdk-fork/${PRBranchName}
|
||||
if ($LASTEXITCODE -ne 0)
|
||||
{
|
||||
Write-Error "Unable to hard reset branch LASTEXITCODE=$($LASTEXITCODE), see command output above."
|
||||
continue
|
||||
}
|
||||
|
||||
# -C0 means to use no extra before or after lines of context to enable us to avoid adjacent line merge conflicts
|
||||
Write-Host "git apply -C0 $TempPatchFile"
|
||||
git apply -C0 $TempPatchFile
|
||||
if ($LASTEXITCODE -ne 0)
|
||||
{
|
||||
Write-Error "Unable to apply diff file LASTEXITCODE=$($LASTEXITCODE), see command output above."
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Host "git -c user.name=`"azure-sdk`" -c user.email=`"azuresdk@microsoft.com`" commit -am `"$($CommitMsg)`""
|
||||
git -c user.name="azure-sdk" -c user.email="azuresdk@microsoft.com" commit -am "$($CommitMsg)"
|
||||
if ($LASTEXITCODE -ne 0)
|
||||
{
|
||||
Write-Error "Unable to commit LASTEXITCODE=$($LASTEXITCODE), see command output above."
|
||||
continue
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if ( Test-Path $TempPatchFile )
|
||||
{
|
||||
Remove-Item $TempPatchFile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} while($needsRetry -and $tryNumber -le $numberOfRetries)
|
||||
|
||||
if ($LASTEXITCODE -ne 0)
|
||||
{
|
||||
Write-Error "Unable to push commit after $($tryNumber) retries LASTEXITCODE=$($LASTEXITCODE), see command output above."
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user