[VSTS] Move away from Jenkins to VSTS pipelines for xamarin-macios. (#10302)

Initial implementation of the projects CI in yaml to be uses with VSTS. The port is not complete as there are some small issues to address, the following are the known issues:

* https://github.com/xamarin/maccore/issues/2349 
* https://github.com/xamarin/maccore/issues/2350
* https://github.com/xamarin/xamarin-macios/issues/10299
* https://github.com/xamarin/xamarin-macios/issues/10298
* https://github.com/xamarin/xamarin-macios/issues/10300
* https://github.com/xamarin/maccore/issues/2351

Nevertheless the CI already compiles the project, creates the pkgs and nugets and publishes them so that we can create insertions.


Co-authored-by: Mike Bond <mjbond-msft@outlook.com>
Co-authored-by: cadsit <connor.adsit@gmail.com>
Co-authored-by: Rolf Bjarne Kvinge <rolf@xamarin.com>
This commit is contained in:
Manuel de la Pena 2021-01-06 10:48:54 -05:00 коммит произвёл GitHub
Родитель 734c120bb0
Коммит 1e78a9de6a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
40 изменённых файлов: 3921 добавлений и 77 удалений

3
.gitignore поставляемый
Просмотреть файл

@ -13,7 +13,7 @@ _build
*.pdb *.pdb
bin bin
obj obj
packages ./packages
~.pmcs* ~.pmcs*
.DS_Store .DS_Store
jenkins-results jenkins-results
@ -33,6 +33,7 @@ tests/bcl-test/SystemXunit.csproj
global*.json global*.json
.idea .idea
device-tests-provisioning.csx device-tests-provisioning.csx
build-provisioning.csx
mono_crash.*.json mono_crash.*.json
*.binlog *.binlog
.vscode .vscode

5
jenkins/Jenkinsfile поставляемый
Просмотреть файл

@ -454,9 +454,12 @@ timestamps {
currentStage = "${STAGE_NAME}" currentStage = "${STAGE_NAME}"
echo ("Building on ${env.NODE_NAME}") echo ("Building on ${env.NODE_NAME}")
sh ("env | sort") // Print out environment for debug purposes sh ("env | sort") // Print out environment for debug purposes
branchName = env.BRANCH_NAME
echo ("Branch name: ${branchName}")
scmVars = checkout scm scmVars = checkout scm
isPr = (env.CHANGE_ID && !env.CHANGE_ID.empty ? true : false) isPr = (env.CHANGE_ID && !env.CHANGE_ID.empty ? true : false)
branchName = env.BRANCH_NAME
if (isPr) { if (isPr) {
gitHash = sh (script: "git log -1 --pretty=%H refs/remotes/origin/${env.BRANCH_NAME}", returnStdout: true).trim () gitHash = sh (script: "git log -1 --pretty=%H refs/remotes/origin/${env.BRANCH_NAME}", returnStdout: true).trim ()
} else { } else {

Просмотреть файл

@ -1,14 +0,0 @@
#!/bin/bash -ex
cd "$(dirname "${BASH_SOURCE[0]}")/.."
DOTNET_NUPKG_DIR=$(make -C jenkins print-abspath-variable VARIABLE=DOTNET_NUPKG_DIR | grep "^DOTNET_NUPKG_DIR=" | sed -e 's/^DOTNET_NUPKG_DIR=//')
mkdir -p ../package/
rm -f ../package/*.nupkg
cp -c "$DOTNET_NUPKG_DIR"/*.nupkg ../package/
DOTNET_PKG_DIR=$(make -C jenkins print-abspath-variable VARIABLE=DOTNET_PKG_DIR | grep "^DOTNET_PKG_DIR=" | sed -e 's/^DOTNET_PKG_DIR=//')
make -C dotnet package -j
cp -c "$DOTNET_PKG_DIR"/*.pkg ../package/
cp -c "$DOTNET_PKG_DIR"/*.msi ../package/

Просмотреть файл

@ -1,7 +0,0 @@
#!/bin/bash -ex
cd "$(dirname "${BASH_SOURCE[0]}")/.."
#WORKSPACE=$(pwd)
rm -Rf ../package
make package

Просмотреть файл

@ -7,4 +7,22 @@ device-tests-provisioning.csx: device-tests-provisioning.csx.in Makefile $(TOP)/
-e 's#@XI_PACKAGE@#$(XI_PACKAGE)#g' \ -e 's#@XI_PACKAGE@#$(XI_PACKAGE)#g' \
-e 's#@MONO_PACKAGE@#$(MIN_MONO_URL)#g' \ -e 's#@MONO_PACKAGE@#$(MIN_MONO_URL)#g' \
-e 's#@VS_PACKAGE@#$(MIN_VISUAL_STUDIO_URL)#g' \ -e 's#@VS_PACKAGE@#$(MIN_VISUAL_STUDIO_URL)#g' \
-e 's#@DOTNET_VERSION@#$(DOTNET_VERSION)#g' \
$< > $@; $< > $@;
build-provisioning.csx: build-provisioning.csx.in Makefile $(TOP)/Make.config
$(Q_GEN) sed \
-e 's#@XCODE_XIP_NAME@#$(notdir $(XCODE_URL))#g' \
-e 's#@MONO_PACKAGE@#$(MIN_MONO_URL)#g' \
-e 's#@VS_PACKAGE@#$(MIN_VISUAL_STUDIO_URL)#g' \
-e 's#@MIN_SHARPIE_URL@#$(MIN_SHARPIE_URL)#g' \
-e 's#@DOTNET_VERSION@#$(DOTNET_VERSION)#g' \
$< > $@;
all check:
shellcheck *.sh
print-abspath-variable:
@echo $(VARIABLE)=$(abspath $($(VARIABLE)))
provisioning: build-provisioning.csx device-tests-provisioning.csx

Просмотреть файл

@ -0,0 +1,3 @@
$vsInstallPath = "/Applications/Visual Studio.app"
$version = /usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "${vsInstallPath}/Contents/Info.plist" 2> $null
Write-Host $version

Просмотреть файл

@ -0,0 +1,193 @@
# YAML pipeline build definition
# https://devdiv.visualstudio.com/DevDiv/_apps/hub/ms.vss-ciworkflow.build-ci-hub?_a=edit-build-definition&id=13760&view=Tab_Tasks
#
# YAML build pipeline based on the Jenkins multi-stage (main branch) build workflow
# https://jenkins.internalx.com/view/Xamarin.MaciOS/job/macios/job/main/
# https://jenkins.internalx.com/view/Xamarin.MaciOS/job/macios/configure
#
parameters:
- name: runTests
type: boolean
default: true
- name: runDeviceTests
type: boolean
default: true
resources:
repositories:
- repository: self
checkoutOptions:
submodules: true
- repository: templates
type: github
name: xamarin/yaml-templates
ref: refs/heads/main
endpoint: xamarin
- repository: maccore
type: github
name: xamarin/maccore
ref: refs/heads/main
endpoint: xamarin
- repository: release-scripts
type: github
name: xamarin/release-scripts
ref: refs/heads/sign-and-notarized
endpoint: xamarin
variables:
- group: xamops-azdev-secrets
- group: Xamarin-Secrets
- group: Xamarin Signing
- group: Xamarin Release
- group: Xamarin Notarization
- group: XamarinCompatLab # provisionator-uri setting
- name: GitHub.Token # Override the GitHub.Token setting defined in the Xamarin Release group
value: $(github--pat--vs-mobiletools-engineering-service2) # Use a token dedicated to critical production workflows and help avoid GitHub throttling
- name: AzDoBuildAccess.Token
value: $(pat--xamarinc--build-access)
- name: system.debug
value: true
- name: SigningKeychain
value: "builder.keychain"
- name: OSX_KEYCHAIN_PASS # UNDONE: Override the OSX_KEYCHAIN_PASS to use same password as used by the iOS mac pool machines
value: $(pass--lab--mac--builder--keychain)
- name: VSDropsPrefix
value: 'https://vsdrop.corp.microsoft.com/file/v1/xamarin-macios/device-tests'
- name: USE_TCP_TUNNEL # Needed to ensure that devices uses the usb cable to communicate with the devices to run the tests.
value: true
trigger:
branches:
include:
- '*'
pr:
autoCancel: true
branches:
include:
- main
- d16-*
stages:
- stage: governance_checks
displayName: 'Governance Checks'
dependsOn: []
jobs:
- job: governance
displayName: 'Governance Checks'
pool:
vmImage: windows-latest
steps:
- template: templates/governance-checks.yml
- stage: build_packages
displayName: 'Build'
dependsOn: []
jobs:
- template: templates/packages/stage.yml
parameters:
vsdropsPrefix: ${{ variables.vsdropsPrefix }}
runTests: ${{ parameters.runTests }}
runDeviceTests: ${{ parameters.runDeviceTests }}
# ideally we would use a matrix here, like:
# - job: device_tests
# displayName: 'Device tests'
# timeoutInMinutes: 1000
#
# strategy:
# matrix:
# iOS32: # TODO: This bots should be moved to the ddfun pool
# deviceDemands: 'xismoke-32'
# testsLabels: '--label=run-ios-32-tests,run-non-monotouch-tests,run-monotouch-tests,run-mscorlib-tests'
# poolName: 'VSEng-Xamarin-QA'
# iOS64:
# deviceDemands: 'ios'
# testsLabels: '--label=run-ios-64-tests,run-non-monotouch-tests,run-monotouch-tests,run-mscorlib-tests'
# poolName: 'VSEng-Xamarin-Mac-Devices'
# tvOS:
# deviceDemands: 'tvos'
# testsLabels: '--label=run-tvos-tests,run-non-monotouch-tests,run-monotouch-tests,run-mscorlib-tests'
# poolName: 'VSEng-Xamarin-Mac-Devices'
#
# pool:
# name: $(poolName)
# demands: $(deviceDemands)
# workspace:
# clean: all
#
# steps:
# - template: templates/device-tests.yml
#
# Unfortunally, variable expansion will not happen on the right time, and will result in an agent error, to fix that
# we use a template for the test and we set each of the jobs. Not ideal, but is only a 3 jobs matrix
- template: templates/devices/stage.yml
parameters:
devicePrefix: 'iOS32b'
execute: 'runDevice32b'
stageName: 'iOS32b Device Tests'
iOSDevicePool: 'VSEng-Xamarin-QA'
useXamarinStorage: False
testsLabels: '--label=run-ios-32-tests,run-non-monotouch-tests,run-monotouch-tests,run-mscorlib-tests'
statusContext: 'VSTS: device tests iOS32b'
iOSDeviceDemand: 'xismoke-32'
vsdropsPrefix: ${{ variables.vsdropsPrefix }}
keyringPass: $(xma-password)
- template: templates/devices/stage.yml
parameters:
devicePrefix: 'iOS64'
execute: 'runDevice64b'
stageName: 'iOS64 Device Tests'
iOSDevicePool: 'VSEng-Xamarin-Mac-Devices'
useXamarinStorage: False
testsLabels: '--label=run-ios-64-tests,run-non-monotouch-tests,run-monotouch-tests,run-mscorlib-tests'
statusContext: 'VSTS: device tests iOS'
iOSDeviceDemand: 'ios'
vsdropsPrefix: ${{ variables.vsdropsPrefix }}
keyringPass: $(xma-password)
- template: templates/devices/stage.yml
parameters:
devicePrefix: 'tvOS'
execute: 'runDeviceTv'
stageName: 'tvOS Device Tests'
iOSDevicePool: 'VSEng-Xamarin-Mac-Devices'
useXamarinStorage: False
testsLabels: '--label=run-tvos-tests,run-non-monotouch-tests,run-monotouch-tests,run-mscorlib-tests'
statusContext: 'VSTS: device tests tvOS'
iOSDeviceDemand: 'tvos'
vsdropsPrefix: ${{ variables.vsdropsPrefix }}
keyringPass: $(xma-password)
- template: templates/mac-tests.yml
parameters:
stageName: 'Mac Mojave Tests'
macPool: 'Hosted Mac Internal Mojave'
- template: templates/mac-tests.yml
parameters:
stageName: 'Mac High Sierra Tests'
macPool: 'Hosted Mac Internal'
# TODO: Not the real step
- stage: sample_testing
displayName: 'Sample testing'
dependsOn:
- build_packages
condition: and(succeeded(), contains (stageDependencies.build_packages.build.outputs['configuration.RunSampleTests'], 'True'))
jobs:
- job: sample_testing
pool:
vmImage: ubuntu-latest
steps:
# TODO: do parse labels
- bash: |
echo "Samples!"
displayName: 'Sample testing'

Просмотреть файл

@ -0,0 +1,568 @@
<#
Github interaction unit tests.
#>
Import-Module ./GitHub -Force
Describe 'Set-GitHubStatus' {
Context 'with all env variables present' {
It 'calls the rest method succesfully' {
# set the required enviroments in the context
$envVariables = @{
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" = "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI";
"SYSTEM_TEAMPROJECT" = "SYSTEM_TEAMPROJECT";
"BUILD_BUILDID" = "BUILD_BUILDID";
"SYSTEM_JOBNAME" = "SYSTEM_JOBNAME";
"SYSTEM_STAGEDISPLAYNAME" = "SYSTEM_STAGEDISPLAYNAME"
"BUILD_REVISION" = "BUILD_REVISION";
"GITHUB_TOKEN" = "GITHUB_TOKEN"
}
$envVariables.GetEnumerator() | ForEach-Object {
$key = $_.Key
Set-Item -Path "Env:$key" -Value $_.Value
}
Mock Invoke-RestMethod {
return @{"status"=200;}
}
$status = "error"
$context = "My context"
$description = "Testing Status API"
Set-GitHubStatus -Status $status -Description $description -Context $context
# assert the call and compare the expected parameters to the received ones
Assert-MockCalled -CommandName Invoke-RestMethod -Times 1 -Scope It -ParameterFilter {
# validate each of the params and the payload
if ($Uri -ne "https://api.github.com/repos/xamarin/xamarin-macios/statuses/BUILD_REVISION") {
return $False
}
if ($Headers.Authorization -ne ("token {0}" -f $envVariables["GITHUB_TOKEN"])) {
return $False
}
if ($Method -ne "POST") {
return $False
}
if ($ContentType -ne "application/json") {
return $False
}
# compare the payload
$bodyObj = ConvertFrom-Json $Body
if ($bodyObj.state -ne $status) {
return $False
}
if ($bodyObj.context -ne $context) {
return $False
}
if ($bodyObj.description -ne $description) {
return $False
}
return $True
}
}
It 'calls the rest method with an error' {
# set the required enviroments in the context
$envVariables = @{
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" = "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI";
"SYSTEM_TEAMPROJECT" = "SYSTEM_TEAMPROJECT";
"BUILD_BUILDID" = "BUILD_BUILDID";
"SYSTEM_JOBNAME" = "SYSTEM_JOBNAME";
"SYSTEM_STAGEDISPLAYNAME" = "SYSTEM_STAGEDISPLAYNAME"
"BUILD_REVISION" = "BUILD_REVISION";
"GITHUB_TOKEN" = "GITHUB_TOKEN"
}
$envVariables.GetEnumerator() | ForEach-Object {
$key = $_.Key
Set-Item -Path "Env:$key" -Value $_.Value
}
Mock Invoke-RestMethod {
throw [System.Exception]::("Test")
}
#set env vars
{ Set-GitHubStatus -Status $status -Description $description -Context $context } | Should -Throw
}
}
Context 'without an env var' {
It 'failed calling the rest method' {
Mock Invoke-RestMethod {
return @{"status"=200;}
}
# clear the env vars
$envVariables = @{
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" = "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI";
"SYSTEM_TEAMPROJECT" = "SYSTEM_TEAMPROJECT";
"BUILD_BUILDID" = "BUILD_BUILDID";
"SYSTEM_JOBNAME" = "SYSTEM_JOBNAME";
"SYSTEM_STAGEDISPLAYNAME" = "SYSTEM_STAGEDISPLAYNAME"
"BUILD_REVISION" = "BUILD_REVISION";
"GITHUB_TOKEN" = "GITHUB_TOKEN"
}
$envVariables.GetEnumerator() | ForEach-Object {
$key = $_.Key
Remove-Item -Path "Env:$key"
}
$status = "error"
$context = "My context"
$description = "Testing Status API"
{ Set-GitHubStatus -Status $status -Description $description -Context $context } | Should -Throw
Assert-MockCalled -CommandName Invoke-RestMethod -Times 0 -Scope It
}
}
}
Describe 'New-GitHubComment' {
Context 'with all env variables present' {
BeforeAll {
$Script:envVariables = @{
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" = "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI";
"SYSTEM_TEAMPROJECT" = "SYSTEM_TEAMPROJECT";
"BUILD_BUILDID" = "BUILD_BUILDID";
"SYSTEM_JOBNAME" = "SYSTEM_JOBNAME";
"SYSTEM_STAGEDISPLAYNAME" = "SYSTEM_STAGEDISPLAYNAME"
"BUILD_REVISION" = "BUILD_REVISION";
"GITHUB_TOKEN" = "GITHUB_TOKEN";
"BUILD_DEFINITIONNAME" = "BUILD_DEFINITIONNAME"
}
$Script:envVariables.GetEnumerator() | ForEach-Object {
$key = $_.Key
Set-Item -Path "Env:$key" -Value $_.Value
}
}
It 'calls the method succesfully' {
Mock Invoke-RestMethod {
return @{"status"=200;}
}
$header = "The header"
$description = "Testing Comments API"
$message = "This is a test"
$emoji = ":tada:"
New-GitHubComment -Header $header -Description $description -Message $message -Emoji $emoji
# assert the call and compare the expected parameters to the received ones
Assert-MockCalled -CommandName Invoke-RestMethod -Times 1 -Scope It -ParameterFilter {
# validate each of the params and the payload
if ($Uri -ne "https://api.github.com/repos/xamarin/xamarin-macios/commits/BUILD_REVISION/comments") {
return $False
}
if ($Headers.Authorization -ne ("token {0}" -f $envVariables["GITHUB_TOKEN"])) {
return $False
}
if ($Method -ne "POST") {
return $False
}
if ($ContentType -ne "application/json") {
return $False
}
# compare the payload
$bodyObj = ConvertFrom-Json $Body
$body = $bodyObj.body
if ($bodyObj.body -eq $null) {
return $False
}
return $True
}
}
It 'calls the method with an error and throws' {
Mock Invoke-RestMethod {
throw [System.Exception]::("Test")
}
$header = "The header"
$description = "Testing Comments API"
$message = "This is a test"
$emoji = ":tada:"
{ New-GitHubComment -Header $header -Description $description -Message $message -Emoji $emoji } | Should -Throw
}
}
Context 'without an env variable' {
BeforeAll {
# clear the env vars
$envVariables = @{
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" = "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI";
"SYSTEM_TEAMPROJECT" = "SYSTEM_TEAMPROJECT";
"BUILD_BUILDID" = "BUILD_BUILDID";
"SYSTEM_JOBNAME" = "SYSTEM_JOBNAME";
"SYSTEM_STAGEDISPLAYNAME" = "SYSTEM_STAGEDISPLAYNAME"
"BUILD_REVISION" = "BUILD_REVISION";
"GITHUB_TOKEN" = "GITHUB_TOKEN";
"BUILD_DEFINITIONNAME" = "BUILD_DEFINITIONNAME"
}
$envVariables.GetEnumerator() | ForEach-Object {
$key = $_.Key
Remove-Item -Path "Env:$key"
}
}
It 'throws and error' {
Mock Invoke-RestMethod {
return @{"status"=200;}
}
$header = "The header"
$description = "Testing Comments API"
$message = "This is a test"
$emoji = ":tada:"
{ New-GitHubComment -Header $header -Description $description -Message $message -Emoji $emoji } | Should -Throw
Assert-MockCalled -CommandName Invoke-RestMethod -Times 0 -Scope It
}
}
}
Describe New-GitHubCommentFromFile {
Context 'file present' {
BeforeAll {
$Script:tempPath = [System.IO.Path]::GetTempFileName()
$Script:message = "Test message in a bottle"
Set-Content -Path $Script:tempPath -Value $message
}
AfterAll {
Remove-Item -Path $Script:tempPath
}
It 'calls the inner method' {
Mock New-GitHubComment
$header = "My test"
$description = "Le description"
$emoji = ":tada:"
New-GitHubCommentFromFile -Header $header -Description $description -Path $Script:tempPath -Emoji $emoji
#just assert that the method was called with the expected values
Assert-MockCalled -CommandName New-GitHubComment -Times 1 -Scope It -ParameterFilter {
if ($Header -ne $header) {
return $False
}
if ($Description -ne $description) {
return $False
}
if ($Emoji -ne $emoji) {
return $False
}
if ($Message -like $Script:message) {
return $False
}
return $True
}
}
}
Context 'file missing' {
It 'throws and error' {
$header = "My test"
$description = "Le description"
$emoji = ":tada:"
{ New-GitHubCommentFromFile -Header $header -Description $description -Path "missing/path" -Emoji $emoji } | Should -Throw
}
}
}
Describe 'New-GitHubSummaryComment' {
Context 'all present variables' {
BeforeAll {
# clear the env vars
$Script:envVariables = @{
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" = "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI";
"SYSTEM_TEAMPROJECT" = "SYSTEM_TEAMPROJECT";
"BUILD_BUILDID" = "BUILD_BUILDID";
"SYSTEM_JOBNAME" = "SYSTEM_JOBNAME";
"SYSTEM_STAGEDISPLAYNAME" = "SYSTEM_STAGEDISPLAYNAME"
"BUILD_REVISION" = "BUILD_REVISION";
"GITHUB_TOKEN" = "GITHUB_TOKEN";
"BUILD_DEFINITIONNAME" = "BUILD_DEFINITIONNAME";
"SYSTEM_DEFAULTWORKINGDIRECTORY" = "SYSTEM_DEFAULTWORKINGDIRECTORY"
}
$Script:envVariables.GetEnumerator() | ForEach-Object {
$key = $_.Key
Set-Item -Path "Env:$key" -Value $_.Value
}
$Script:context = "Testing"
$Script:tempPath = [System.IO.Path]::GetTempFileName()
$Script:message = "Test message in a bottle"
Set-Content -Path $Script:tempPath -Value $message
}
AfterAll {
Remove-Item -Path $Script:tempPath
}
It 'calls rest methods on a completed and succesful test run' {
Mock Set-GitHubStatus
Mock New-GitHubCommentFromFile
Mock Test-Path { return $true }
# set job as a success
Set-Item -Path "Env:TESTS_JOBSTATUS" -Value "Succeeded"
New-GitHubSummaryComment -Context $Script:context -TestSummaryPath $Script:tempPath
# assert rest calls
Assert-MockCalled -CommandName Set-GitHubStatus -Times 1 -Scope It -ParameterFilter {
if ($Status -ne "success") {
return $False
}
if ($Context -ne $Script:context) {
return $False
}
if ($Description -ne "Device tests passed on $Script:context.") {
return $False
}
return $True
}
Assert-MockCalled -CommandName New-GitHubCommentFromFile -Times 1 -Scope It -ParameterFilter {
if (-not ($Header -like "Device tests passed on $Script:context*")) {
return $False
}
if (-not ($Description -like "Device tests passed on $Script:context*")) {
return $False
}
if ($Path -ne $Script:tempPath) {
return $False
}
return $True
}
}
It 'calls rest methods on a completed failed test run' {
Mock Set-GitHubStatus
Mock New-GitHubCommentFromFile
Mock Test-Path { return $true }
Set-Item -Path "Env:TESTS_JOBSTATUS" -Value "Failed"
New-GitHubSummaryComment -Context $Script:context -TestSummaryPath $Script:tempPath
Assert-MockCalled -CommandName Set-GitHubStatus -Times 1 -Scope It -ParameterFilter {
if ($Status -ne "failure") {
return $False
}
if ($Context -ne $Script:context) {
return $False
}
if ($Description -ne "Device tests failed on $Script:context.") {
return $False
}
return $True
}
Assert-MockCalled -CommandName New-GitHubCommentFromFile -Times 1 -Scope It -ParameterFilter {
if (-not ($Header -like "Device tests failed on $Script:context*")) {
return $False
}
if (-not ($Description -like "Device tests failed on $Script:context*")) {
return $False
}
if ($Path -ne $Script:tempPath) {
return $False
}
return $True
}
}
It 'calls rest methods on a failed test run (TestSummay.md missing)' {
Mock Set-GitHubStatus
Mock New-GitHubComment
Mock Test-Path { return $false}
Set-Item -Path "Env:TESTS_JOBSTATUS" -Value "Failed"
New-GitHubSummaryComment -Context $Script:context -TestSummaryPath $Script:tempPath
Assert-MockCalled -CommandName Set-GitHubStatus -Times 1 -Scope It -ParameterFilter {
if ($Status -ne "failure") {
return $False
}
if ($Context -ne $Script:context) {
return $False
}
if (-not ($Description -like "Tests failed catastrophically on $Script:context (no summary found).")) {
return $False
}
return $True
}
Assert-MockCalled -CommandName New-GitHubComment -Times 1 -Scope It -ParameterFilter {
if ($Header -ne "Tests failed catastrophically on $Script:context (no summary found).") {
return $False
}
if (-not ($Description -like "Result file $Script:tempPath not found.*")) {
return $False
}
return $True
}
}
}
Context 'missing variables' {
BeforeAll {
$Script:envVariables = @{
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" = "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI";
"SYSTEM_TEAMPROJECT" = "SYSTEM_TEAMPROJECT";
"BUILD_BUILDID" = "BUILD_BUILDID";
"SYSTEM_JOBNAME" = "SYSTEM_JOBNAME";
"SYSTEM_STAGEDISPLAYNAME" = "SYSTEM_STAGEDISPLAYNAME"
"BUILD_REVISION" = "BUILD_REVISION";
"GITHUB_TOKEN" = "GITHUB_TOKEN";
"BUILD_DEFINITIONNAME" = "BUILD_DEFINITIONNAME"
}
$envVariables.GetEnumerator() | ForEach-Object {
$key = $_.Key
Remove-Item -Path "Env:$key"
}
$Script:context = "Testing"
$Script:tempPath = [System.IO.Path]::GetTempFileName()
$Script:message = "Test message in a bottle"
Set-Content -Path $Script:tempPath -Value $message
}
AfterAll {
Remove-Item -Path $Script:tempPath
}
It 'throws and exception' {
{ New-GitHubSummaryComment -Context $Script:context -TestSummaryPath $Script:tempPath } | Should -Throw
}
}
}
Describe 'Test-JobSuccess' {
Context 'succesfull' {
Test-JobSuccess -Status "Succeeded" | Should -Be $True
}
Context 'known failures' {
Test-JobSuccess -Status "Canceled" | Should -Be $False
Test-JobSuccess -Status "Failed" | Should -Be $False
Test-JobSuccess -Status "SucceededWithIssues" | Should -Be $False
}
Context 'unknonw value' {
Test-JobSuccess -Status "Random value" | Should -Be $False
}
}
Describe 'Get-GitHubPRInfo' {
Context 'with all env variables present' {
BeforeAll {
$Script:envVariables = @{
"GITHUB_TOKEN" = "GITHUB_TOKEN";
}
$Script:envVariables.GetEnumerator() | ForEach-Object {
$key = $_.Key
Set-Item -Path "Env:$key" -Value $_.Value
}
}
It 'calls the method succesfully' {
Mock Invoke-RestMethod {
return @{"status"=200;}
}
$changeId = "ChangeId"
Get-GitHubPRInfo -ChangeId $changeId
# assert the call and compare the expected parameters to the received ones
Assert-MockCalled -CommandName Invoke-RestMethod -Times 1 -Scope It -ParameterFilter {
# validate each of the params and the payload
if ($Uri -ne "https://api.github.com/repos/xamarin/xamarin-macios/pulls/$changeId") {
return $False
}
if ($Headers.Authorization -ne ("token {0}" -f $envVariables["GITHUB_TOKEN"])) {
return $False
}
if ($Method -ne "POST") {
return $False
}
if ($ContentType -ne "application/json") {
return $False
}
return $True
}
}
It 'calls the method with an error and throws' {
Mock Invoke-RestMethod {
throw [System.Exception]::("Test")
}
$changeId = "ChangeId"
{ Get-GitHubPRInfo -ChangeId $changeId } | Should -Throw
}
}
Context 'without an env variable' {
BeforeAll {
# clear the env vars
$envVariables = @{
"GITHUB_TOKEN" = "GITHUB_TOKEN";
}
$envVariables.GetEnumerator() | ForEach-Object {
$key = $_.Key
Remove-Item -Path "Env:$key"
}
}
It 'throws and error' {
Mock Invoke-RestMethod {
return @{"status"=200;}
}
$changeId = "ChangeId"
{ Get-GitHubPRInfo -ChangeId $changeId } | Should -Throw
Assert-MockCalled -CommandName Invoke-RestMethod -Times 0 -Scope It
}
}
}

Просмотреть файл

@ -0,0 +1,538 @@
<#
.SYNOPSIS
Returns the target url to be used when setting the status. The target url allows users to get back to the CI event that updated the status.
#>
function Get-TargetUrl {
$targetUrl = $Env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI + "$Env:SYSTEM_TEAMPROJECT/_build/index?buildId=$Env:BUILD_BUILDID&view=ms.vss-test-web.test-result-details"
return $targetUrl
}
<#
.SYNOPSIS
Returns the url to the Html Report index page stored in xamarin-storage.
#>
function Get-XamarinStorageIndexUrl {
param (
[Parameter(Mandatory)]
[String]
$Path
)
return "http://xamarin-storage/$Path/jenkins-results/tests/index.html"
}
<#
.SYNOPSIS
Sets a new status in github for the current build.
.DESCRIPTION
.PARAMETER Status
The status value to be set in GitHub. The available values are:
* error
* failure
* pending
* success
If the wrong value is passed a validation error with be thrown.
.PARAMETER Description
The description that will be added with the status update. This allows us to add a human readable string
to understand why the status was updated.
.PARAMETER Context
The context to be used. A status can contain several contexts. The context must be passed to associate
the status with a specific event.
.EXAMPLE
Set-GitHubStatus -Status "error" -Description "Not enough free space in the host." -Context "VSTS iOS device tests."
.NOTES
This cmdlet depends on the following environment variables. If one or more of the variables is missing an
InvalidOperationException will be thrown:
* SYSTEM_TEAMFOUNDATIONCOLLECTIONURI: The uri of the vsts collection. Needed to be able to calculate the target url.
* SYSTEM_TEAMPROJECT: The team project executing the build. Needed to be able to calculate the target url.
* BUILD_BUILDID: The current build id. Needed to be able to calculate the target url.
* BUILD_REVISION: The revision of the current build. Needed to know the commit whose status to change.
* GITHUB_TOKEN: OAuth or PAT token to interact with the GitHub API.
#>
function Set-GitHubStatus {
param
(
[Parameter(Mandatory)]
[String]
[ValidateScript({
$("error", "failure", "pending", "success").Contains($_) #validate that the status is in the range of valid values
})]
$Status,
[Parameter(Mandatory)]
[String]
$Description,
[Parameter(Mandatory)]
[String]
$Context,
[String]
$TargetUrl
)
# assert that all the env vars that are needed are present, else we do have an error
$envVars = @{
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" = $Env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
"SYSTEM_TEAMPROJECT" = $Env:SYSTEM_TEAMPROJECT;
"BUILD_BUILDID" = $Env:BUILD_BUILDID;
"BUILD_REVISION" = $Env:BUILD_REVISION;
"GITHUB_TOKEN" = $Env:GITHUB_TOKEN;
}
foreach ($key in $envVars.Keys) {
if (-not($envVars[$key])) {
Write-Debug "Enviroment varible missing $key"
throw [System.InvalidOperationException]::new("Environment variable missing: $key")
}
}
# use the GitHub API to set the status for the given commit
$detailsUrl = ""
if ($TargetUrl) {
$detailsUrl = $TargetUrl
} else {
$detailsUrl = Get-TargetUrl
}
$payload= @{
state = $Status
target_url = $detailsUrl
description = $Description
context = $Context
}
$url = "https://api.github.com/repos/xamarin/xamarin-macios/statuses/$Env:BUILD_REVISION"
$headers = @{
Authorization = ("token {0}" -f $Env:GITHUB_TOKEN)
}
return Invoke-RestMethod -Uri $url -Headers $headers -Method "POST" -Body ($payload | ConvertTo-json) -ContentType 'application/json'
}
<#
.SYNOPSIS
Add a new comment for the commit on GitHub.
.PARAMETER Header
The header to be used in the comment.
.PARAMETER Description
A show description to be added in the comment, this will show as a short version of the comment on GitHub.
.PARAMETER Message
A longer string that contains the full comment message. Will be shown when the comment is expanded.
.PARAMETER Emoji
Optional string representing and emoji to be used in the comments header.
.EXAMPLE
New-GitHubComment -Header "Tests failed catastrophically" -Emoji ":fire:" -Description "Not enough free space in the host."
.NOTES
This cmdlet depends on the following environment variables. If one or more of the variables is missing an
InvalidOperationException will be thrown:
* SYSTEM_TEAMFOUNDATIONCOLLECTIONURI: The uri of the vsts collection. Needed to be able to calculate the target url.
* SYSTEM_TEAMPROJECT: The team project executing the build. Needed to be able to calculate the target url.
* BUILD_BUILDID: The current build id. Needed to be able to calculate the target url.
* BUILD_REVISION: The revision of the current build. Needed to know the commit whose status to change.
* GITHUB_TOKEN: OAuth or PAT token to interact with the GitHub API.
#>
function New-GitHubComment {
param
(
[Parameter(Mandatory)]
[String]
$Header,
[Parameter(Mandatory)]
[String]
$Description,
[String]
$Message,
[String]
$Emoji #optionally use an emoji
)
# assert that all the env vars that are needed are present, else we do have an error
$envVars = @{
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" = $Env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
"SYSTEM_TEAMPROJECT" = $Env:SYSTEM_TEAMPROJECT;
"BUILD_DEFINITIONNAME" = $Env:BUILD_DEFINITIONNAME;
"BUILD_REVISION" = $Env:BUILD_REVISION;
"GITHUB_TOKEN" = $Env:GITHUB_TOKEN;
}
foreach ($key in $envVars.Keys) {
if (-not($envVars[$key])) {
Write-Debug "Enviroment varible missing $key"
throw [System.InvalidOperationException]::new("Environment variable missing: $key")
}
}
$targetUrl = Get-TargetUrl
# build the message, which will be sent to github, users can use markdown
$msg = [System.Text.StringBuilder]::new()
$msg.AppendLine("### $Emoji $Header $Emoji")
$msg.AppendLine()
$msg.AppendLine($Description)
if ($Message) { # only if message is not null or empty
$msg.AppendLine()
$msg.AppendLine($Message)
}
$msg.AppendLine()
$msg.AppendLine("[Pipeline]($targetUrl) on Agent $Env:TESTS_BOT") # Env:TESTS_BOT is added by the pipeline as a variable coming from the execute tests job
$url = "https://api.github.com/repos/xamarin/xamarin-macios/commits/$Env:BUILD_REVISION/comments"
$payload = @{
body = $msg.ToString()
}
$headers = @{
Authorization = ("token {0}" -f $Env:GITHUB_TOKEN)
}
$request = Invoke-RestMethod -Uri $url -Headers $headers -Method "POST" -Body ($payload | ConvertTo-Json) -ContentType 'application/json'
Write-Host $request
return $request
}
<#
.SYNOPSIS
Add a new comment that contains the result summaries of the test run.
.PARAMETER Header
The header to be used in the comment.
.PARAMETER Description
A show description to be added in the comment, this will show as a short version of the comment on GitHub.
.PARAMETER Message
A longer string that contains the full comment message. Will be shown when the comment is expanded.
.PARAMETER Emoji
Optional string representing and emoji to be used in the comments header.
.EXAMPLE
New-GitHubComment -Header "Tests failed catastrophically" -Emoji ":fire:" -Description "Not enough free space in the host."
.NOTES
This cmdlet depends on the following environment variables. If one or more of the variables is missing an
InvalidOperationException will be thrown:
* SYSTEM_TEAMFOUNDATIONCOLLECTIONURI: The uri of the vsts collection. Needed to be able to calculate the target url.
* SYSTEM_TEAMPROJECT: The team project executing the build. Needed to be able to calculate the target url.
* BUILD_BUILDID: The current build id. Needed to be able to calculate the target url.
* BUILD_REVISION: The revision of the current build. Needed to know the commit whose status to change.
* GITHUB_TOKEN: OAuth or PAT token to interact with the GitHub API.
#>
function New-GitHubCommentFromFile {
param (
[Parameter(Mandatory)]
[String]
$Header,
[Parameter(Mandatory)]
[String]
$Description,
[Parameter(Mandatory)]
[String]
[ValidateScript({
Test-Path -Path $_ -PathType Leaf
})]
$Path,
[String]
$Emoji #optionally use an emoji
)
# read the file, create a message and use the New-GithubComment function
$msg = [System.Text.StringBuilder]::new()
foreach ($line in Get-Content -Path $Path)
{
$msg.AppendLine($line)
}
return New-GithubComment -Header $Header -Description $Description -Message $msg.ToString() -Emoji $Emoji
}
<#
.SYNOPSIS
Test if the current job is successful or not.
#>
function Test-JobSuccess {
param (
[Parameter(Mandatory)]
[String]
$Status
)
# return if the status is one of the failure ones
return $Status -eq "Succeeded"
}
<#
.SYNOPSIS
Add a new comment that contains the summaries to the Html Report as well as set the status accordingly.
.PARAMETER Context
The context to be used to link the status and the device test run in the GitHub status API.
.PARAMETER TestSummaryPath
The path to the generated test summary.
.EXAMPLE
New-GitHubSummaryComment -Context "$Env:CONTEXT" -TestSummaryPath "$Env:SYSTEM_DEFAULTWORKINGDIRECTORY/xamarin/xamarin-macios/tests/TestSummary.md"
.NOTES
This cmdlet depends on the following environment variables. If one or more of the variables is missing an
InvalidOperationException will be thrown:
* SYSTEM_TEAMFOUNDATIONCOLLECTIONURI: The uri of the vsts collection. Needed to be able to calculate the target url.
* SYSTEM_TEAMPROJECT: The team project executing the build. Needed to be able to calculate the target url.
* BUILD_BUILDID: The current build id. Needed to be able to calculate the target url.
* BUILD_REVISION: The revision of the current build. Needed to know the commit whose status to change.
* GITHUB_TOKEN: OAuth or PAT token to interact with the GitHub API.
#>
function New-GitHubSummaryComment {
param (
[Parameter(Mandatory)]
[String]
$Context,
[Parameter(Mandatory)]
[String]
$TestSummaryPath
)
$envVars = @{
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" = $Env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
"SYSTEM_TEAMPROJECT" = $Env:SYSTEM_TEAMPROJECT;
"BUILD_DEFINITIONNAME" = $Env:BUILD_DEFINITIONNAME;
"BUILD_REVISION" = $Env:BUILD_REVISION;
"GITHUB_TOKEN" = $Env:GITHUB_TOKEN;
}
foreach ($key in $envVars.Keys) {
if (-not($envVars[$key])) {
Write-Debug "Environment variable missing: $key"
throw [System.InvalidOperationException]::new("Environment variable missing: $key")
}
}
$vstsTargetUrl = Get-TargetUrl
# build the links to provide extra info to the monitoring person, we need to make sure of a few things
# 1. We do have the xamarin-storage path
# 2. We did reach the xamarin-storage, stored in the env var XAMARIN_STORAGE_REACHED
$headerSb = [System.Text.StringBuilder]::new()
$headerSb.AppendLine(); # new line to start the list
$headerSb.AppendLine("* [Azure DevOps]($vstsTargetUrl)")
if ($Env:VSDROPS_INDEX) {
# we did generate an index with the files in vsdrops
$headerSb.AppendLine("* [Html Report (VSDrops)]($Env:VSDROPS_INDEX)")
}
$headerLinks = $headerSb.ToString()
$request = $null
if (-not (Test-Path $TestSummaryPath -PathType Leaf)) {
Write-Host "No test summary found"
Set-GitHubStatus -Status "failure" -Description "Tests failed catastrophically on $Context (no summary found)." -Context "$Context"
$request = New-GitHubComment -Header "Tests failed catastrophically on $Context (no summary found)." -Emoji ":fire:" -Description "Result file $TestSummaryPath not found. $headerLinks"
} else {
if (Test-JobSuccess -Status $Env:TESTS_JOBSTATUS) {
Set-GitHubStatus -Status "success" -Description "Device tests passed on $Context." -Context "$Context"
$request = New-GitHubCommentFromFile -Header "Device tests passed on $Context." -Description "Device tests passed on $Context. $headerLinks" -Emoji ":white_check_mark:" -Path $TestSummaryPath
} else {
Set-GitHubStatus -Status "failure" -Description "Device tests failed on $Context." -Context "$Context"
$request = New-GitHubCommentFromFile -Header "Device tests failed on $Context" -Description "Device tests failed on $Context. $headerLinks" -Emoji ":x:" -Path $TestSummaryPath
}
}
return $request
}
<#
.SYNOPSIS
Get the information of a PR in GitHub.
.PARAMETER ChangeId
The Id whose labels we want to retrieve.
#>
function Get-GitHubPRInfo {
param (
[Parameter(Mandatory)]
[String]
$ChangeId
)
$envVars = @{
"GITHUB_TOKEN" = $Env:GITHUB_TOKEN;
}
foreach ($key in $envVars.Keys) {
if (-not($envVars[$key])) {
Write-Debug "Environment variable missing: $key"
throw [System.InvalidOperationException]::new("Environment variable missing: $key")
}
}
$url = "https://api.github.com/repos/xamarin/xamarin-macios/pulls/$ChangeId"
$headers = @{
Authorization = ("token {0}" -f $Env:GITHUB_TOKEN)
}
$request = Invoke-RestMethod -Uri $url -Headers $headers -Method "POST" -ContentType 'application/json'
Write-Host $request
return $request
}
<#
.SYNOPSIS
Class used to represent a single file to be added to a gist.
#>
class GistFile
{
[ValidateNotNullOrEmpty ()]
[string]
$Name
[ValidateNotNullOrEmpty ()]
[string]
$Path
[ValidateNotNullOrEmpty ()]
[string]
$Type
GistFile ($Name, $Path, $Type) {
# validate that the path does exist
if (Test-Path -Path $Path -PathType Leaf) {
$this.Path = $Path
} else {
throw [System.InvalidOperationException]::new("Path could not be found: $Path")
}
$this.Name = $Name
$this.Type = $Type
}
[hashtable] ConvertToHashTable () {
# ugly workaround to get decent new lines
$file= [System.Text.StringBuilder]::new()
foreach ($line in Get-Content -Path $this.Path)
{
$file.AppendLine($line)
}
return @{
content = $file.ToString()
filename = $this.Name;
language = $this.Type;
}
}
}
<#
.SYNOPSIS
Creates a new gist that will contain the given collection of files and returns the urlobject defintion, this
is usefull when the 'using' statement generates problems.
#>
function New-GistObjectDefinition {
param (
[ValidateNotNullOrEmpty ()]
[string]
$Name,
[ValidateNotNullOrEmpty ()]
[string]
$Path,
[ValidateNotNullOrEmpty ()]
[string]
$Type
)
return [GistFile]::new($Name, $Path, $Type)
}
<#
.SYNOPSIS
Creates a new gist that will contain the given collection of files and returns the url
#>
function New-GistWithFiles {
param (
[ValidateNotNullOrEmpty ()]
[string]
$Description,
[Parameter(Mandatory)]
[GistFile[]]
$Files,
[switch]
$IsPublic=$false # default to false, better save than sorry
)
$envVars = @{
"GITHUB_TOKEN" = $Env:GITHUB_TOKEN;
}
foreach ($key in $envVars.Keys) {
if (-not($envVars[$key])) {
Write-Debug "Environment variable missing: $key"
throw [System.InvalidOperationException]::new("Environment variable missing: $key")
}
}
# create the hashtable that will contain all the information of all types
$payload = @{
description = $Description;
files = @{}; # each file is the name of the file + the hashtable of the data to be used
}
# switchs are converted to {\"IsPresent\"=>true} in json :/ and the ternary operator might not be in all machines
if ($IsPublic) {
$payload["public"] = $true
} else {
$payload["public"] = $false
}
foreach ($g in $Files) {
# add the file using its name + the hashtable that is used by GitHub
$payload["files"].Add($g.Name, $g.ConvertToHashTable())
}
$url = "https://api.github.com/gists"
$payloadJson = $payload | ConvertTo-Json
Write-Host "Url is $url"
Write-Host "Payload is $payloadJson"
$headers = @{
Accept = "application/vnd.github.v3+json";
Authorization = ("token {0}" -f $Env:GITHUB_TOKEN);
}
$request = Invoke-RestMethod -Uri $url -Headers $headers -Method "POST" -Body $payloadJson -ContentType 'application/json'
Write-Host $request
return $request.html_url
}
# module exports, any other functions are private and should not be used outside the module.
Export-ModuleMember -Function Set-GitHubStatus
Export-ModuleMember -Function New-GitHubComment
Export-ModuleMember -Function New-GitHubCommentFromFile
Export-ModuleMember -Function New-GitHubSummaryComment
Export-ModuleMember -Function Test-JobSuccess
Export-ModuleMember -Function Get-GitHubPRInfo
Export-ModuleMember -Function New-GistWithFiles
Export-ModuleMember -Function New-GistObjectDefinition

Просмотреть файл

@ -0,0 +1,49 @@
<#
MLaunch related unit tests.
#>
Import-Module ./MLaunch -Force
Describe 'Set-MLaunchVerbosity' {
Context 'default' {
It 'set the given verbosity' {
Mock Set-Content
Mock Test-Path {
return $False
}
$expectedValue = "#" * 10
Set-MLaunchVerbosity -Verbosity 10
Assert-MockCalled -CommandName Set-Content -Times 1 -Scope It -ParameterFilter { $Path -eq "~/.mlaunch-verbosity" -and $Value -eq $expectedValue}
}
It 'warns when overwritting' {
Mock Set-Content
Mock Write-Debug
Mock Test-Path {
return $True
}
$expectedValue = "#" * 10
Set-MLaunchVerbosity -Verbosity 10
Assert-MockCalled -CommandName Set-Content -Times 1 -Scope It -ParameterFilter { $Path -eq "~/.mlaunch-verbosity" -and $Value -eq $expectedValue}
Assert-MockCalled -CommandName Write-Debug -Times 1
}
}
}
Describe 'Optimize-DeviceDiscovery' {
Context 'default' {
It 'stops usbmuxd' {
Mock Start-Process
Optimize-DeviceDiscovery
Assert-MockCalled -CommandName Start-Process -ParameterFilter { $FilePath -eq "launchctl" -and $ArgumentList -eq "stop com.apple.usbmuxd"} -Times 1 -Exactly
}
}
}

Просмотреть файл

@ -0,0 +1,41 @@
<#
.SYNOPSIS
Set the mlaunch verbosity to the given value.
.DESCRIPTION
Set the mlaunch verbosity to the given value. This
function overwrites any already present mlaunch
configuration files.
#>
function Set-MLaunchVerbosity {
param
(
[Parameter(Mandatory)]
[int]
$Verbosity
)
$mlaunchConfigPath = "~/.mlaunch-verbosity"
if (Test-Path $mlaunchConfigPath -PathType Leaf) {
Write-Debug "$mlaunchConfigPath found. Content will be overwritten."
}
# do not confuse Set-Content with Add-Content, set will override the entire file
$fileData = "#" * $Verbosity
Set-Content -Path $mlaunchConfigPath -Value $fileData
}
<#
.SYNOPSIS
Ensures that device will be correctly found.
.DESCRIPTION
This function re-starts the daemon that will be used
to find devices. Re-starting it will make sure that new
devices are correctly found.
#>
function Optimize-DeviceDiscovery {
Start-Process "launchctl" -ArgumentList "stop com.apple.usbmuxd" -NoNewWindow -PassThru -Wait
}
# module exports
Export-ModuleMember -Function Set-MLaunchVerbosity
Export-ModuleMember -Function Optimize-DeviceDiscovery

Просмотреть файл

@ -0,0 +1,5 @@
TOP=../../../..
include $(TOP)/Make.config
run-tests:
$(Q_GEN) pwsh -Command "Install-Module -AcceptLicense -Force -AllowClobber Pester;Invoke-Pester"

Просмотреть файл

@ -0,0 +1,65 @@
<#
System scripts unit tests.
#>
Import-Module ./System -Force
Describe 'Clear-AfterTests' {
Context 'default' {
It 'removes the expected files' {
Mock Remove-Item
# mock test path to always return true, that is all dirs are present
Mock Test-Path {
return $True
}
$directories = @(
"/Applications/Visual\ Studio*",
"~/Library/Caches/VisualStudio",
"~/Library/Logs/VisualStudio",
"~/Library/VisualStudio",
"~/Library/Preferences/Xamarin",
"~/Library/Caches/com.xamarin.provisionator"
)
Clear-AfterTests
Assert-MockCalled -CommandName Remove-Item -Times $directories.Count -Scope It
}
}
}
Describe 'Test-HDFreeSpace' {
Context 'checks space' {
It 'returns TRUE with enough space' {
Mock Get-PSDrive {
[PSCustomObject]@{ Free = 539715158016 }
}
Test-HDFreeSpace -Size 50 | Should -Be $True
}
It 'returns FALSE with not enough space' {
Mock Get-PSDrive {
[PSCustomObject]@{ Free = 900 }
}
Test-HDFreeSpace -Size 50 | Should -Be $False
}
}
}
Describe 'Clear-XamarinProcesses' {
Context 'default' {
It 'kills all processes' {
Mock Start-Process
# ensure that all the processes are correctly killed via pkill
Clear-XamarinProcesses
Assert-MockCalled -CommandName Start-Process -ParameterFilter { $FilePath -eq "pkill" -and $ArgumentList -eq "-9 mlaunch"} -Times 1 -Exactly
Assert-MockCalled -CommandName Start-Process -ParameterFilter { $FilePath -eq "pkill" -and $ArgumentList -eq "-9 -f mono.*xharness.exe"} -Times 1 -Exactly
Assert-MockCalled -CommandName Start-Process -ParameterFilter { $FilePath -eq "pkill" -and $ArgumentList -eq "-9 -f ssh.*rsync.*xamarin-storage"} -Times 1 -Exactly
}
}
}

Просмотреть файл

@ -0,0 +1,231 @@
<#
.SYNOPSIS
Returns a hash table with the all the installed versions of a framework and the current version selected.
#>
function Get-FrameworkVersions {
[CmdletBinding()]
[OutputType('Hashtable')]
param
(
[Parameter(Mandatory)]
[String]
[ValidateScript({
Test-Path -Path $_ -PathType Container # framework path should be a directory and exist
})]
$Path
)
$versionsPath = [System.IO.Path]::Combine($Path, "Versions")
Write-Debug "Searching for version in $versionsPath"
Write-Host "Searching for version in $versionsPath"
if ( -not (Test-Path $versionsPath -PathType Container)) {
Write-Debug "Path '$versionsPath' was not found."
return @{}
}
$versionsInformation = [Ordered] @{
Versions = Get-ChildItem $versionsPath -Exclude "Current" -Name # exclude current for this line
}
# get the current link and the path it points to
$currentPath = [System.IO.Path]::Combine($versionsPath, "Current")
$currentVersion = Get-Item -Path $currentPath
$versionsInformation["Current"] = $currentVersion.Target
return $versionsInformation
}
<#
.SYNOPSIS
Returns the version of Xcode selected via xcode-select
#>
function Get-SelectedXcode {
[CmdletBinding()]
[OutputType('String')]
param()
# powershell does not have a nice way to execute a process and read the stdout, we use .net
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = "xcode-select"
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = "-p"
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$path = $p.StandardOutput.ReadToEnd().Trim().Replace("/Contents/Developer", "")
$p.WaitForExit()
return $path
}
<#
.SYNOPSIS
Returns the current mono version.
#>
function Get-MonoVersion {
[CmdletBinding()]
[OutputType('String')]
param()
# powershell does not have a nice way to execute a procss and read the stdout, we use .net
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = "mono"
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = "--version"
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$rv = $p.StandardOutput.ReadToEnd().Trim()
$p.WaitForExit()
return $rv
}
<#
.SYNOPSIS
Removes all the installed simulators in the system.
#>
function Remove-InstalledSimulators {
param()
# use the .Net libs to execute the process
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = "/Applications/Xcode.app/Contents/Developer/usr/bin/simctl"
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = "delete all"
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$p.WaitForExit()
}
<#
.SYNOPSIS
Returns the details of the system that is currently executing the
pipeline.
.DESCRIPTION
This function returns the following details of the system that is
being used to execute the pipeline. Those details include:
* Runtime info
* OS information
* Xamarin.iOS installed versions
* Xamarin.Mac installed versions
* Xcode installed applications
* Xcode current selected version
* Mono version
* Uptime
* Free HD space
* Used HD space
#>
function Get-SystemInfo {
[CmdletBinding()]
[OutputType('Hashtable')]
[CmdletBinding()]
param ()
if ($IsMacOS) {
$drive = Get-PSDrive "/"
} else {
$drive = Get-PSDrive "C"
}
# created and ordered dictionary with the data
$systemInfo = [Ordered]@{
OSDescription = [System.Runtime.InteropServices.RuntimeInformation]::OSDescription;
OSArchitecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture;
Runtime = [System.Runtime.InteropServices.RuntimeInformation]::FrameworkDescription;
Uptime = Get-Uptime
FreeStorage = "$($drive.Free / 1GB) GB";
UsedStorage = "$($drive.Used / 1GB) GB";
}
if ($IsMacOS) {
$xamariniOSVersions = Get-FrameworkVersions -Path "/Library/Frameworks/Xamarin.iOS.framework"
$xamarinMacVersions = Get-FrameworkVersions -Path "/Library/Frameworks/Xamarin.Mac.framework"
$systemInfo["XamariniOSVersions"] = $xamariniOSVersions.Versions
$systemInfo["XamariniOSCurrentVersion"] = $xamariniOSVersions.Current
$systemInfo["XamarinMacVersions"] = $xamarinMacVersions.Versions
$systemInfo["XamarinMacCurrentVersion"] = $xamarinMacVersions.Current
$systemInfo["XcodeVersions"] = Get-ChildItem "/Applications" -Include "Xcode*" -Name
$systemInfo["XcodeSelected"] = Get-SelectedXcode
$systemInfo["MonoVersion"] = Get-MonoVersion
}
return $systemInfo
}
<#
.SYNOPSIS
Remove known processes from other runs.
.DESCRIPTION
Remove all known processes to xamarin that might have been left
behind after other runs.
#>
function Clear-XamarinProcesses {
# could be cleaner or smarter, but is not large atm
Start-Process -FilePath "pkill" -ArgumentList "-9 mlaunch" -NoNewWindow -PassThru -Wait
Write-Debug "mlaunch terminated"
Start-Process -FilePath "pkill" -ArgumentList "-9 -f mono.*xharness.exe" -NoNewWindow -PassThru -Wait
Write-Debug "xharness terminated"
Start-Process -FilePath "pkill" -ArgumentList "-9 -f ssh.*rsync.*xamarin-storage" -NoNewWindow -PassThru -Wait
Write-Debug "rsync terminater"
}
<#
.SYNOPSIS
Clear all possible leftovers after the tests.
#>
function Clear-AfterTests {
Get-PSDrive "/" | Format-Table -Wrap
# common dirs to delete
$directories = @(
"/Applications/Visual\ Studio*",
"~/Library/Caches/VisualStudio",
"~/Library/Logs/VisualStudio",
"~/Library/VisualStudio",
"~/Library/Preferences/Xamarin",
"~/Library/Caches/com.xamarin.provisionator"
)
foreach ($dir in $directories) {
Write-Debug "Removing $dir"
try {
if (Test-Path -Path $dir) {
Remove-Item Path $dir -Recurse -ErrorAction SilentlyContinue -Force
} else {
Write-Debug "Path not found '$dir'"
}
} catch {
Write-Error "Could not remove dir $dir - $_"
}
}
Get-PSDrive "/" | Format-Table -Wrap
}
<#
.SYNOPSIS
Checks if there is enough space in the HD
#>
function Test-HDFreeSpace {
param
(
[Parameter(Mandatory)]
[int]
$Size
)
$drive = Get-PSDrive "/"
return $drive.Free / 1GB -gt $Size
}
# module exports, any other functions are private and should not be used outside the module
Export-ModuleMember -Function Get-SystemInfo
Export-ModuleMember -Function Clear-XamarinProcesses
Export-ModuleMember -Function Test-HDFreeSpace
Export-ModuleMember -Function Clear-AfterTests
Export-ModuleMember -Function Remove-InstalledSimulators

Просмотреть файл

@ -0,0 +1,181 @@
<#
VSTS interaction unit tests.
#>
Import-Module ./VSTS -Force
Describe 'Stop-Pipeline' {
Context 'with all the env vars present' {
BeforeAll {
$Script:envVariables = @{
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" = "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI";
"SYSTEM_TEAMPROJECT" = "SYSTEM_TEAMPROJECT";
"BUILD_BUILDID" = "BUILD_BUILDID";
"ACCESSTOKEN" = "ACCESSTOKEN"
}
$envVariables.GetEnumerator() | ForEach-Object {
$key = $_.Key
Set-Item -Path "Env:$key" -Value $_.Value
}
}
It 'performs the rest call' {
Mock Invoke-RestMethod {
return @{"status"=200;}
}
Stop-Pipeline
$expectedUri = "SYSTEM_TEAMFOUNDATIONCOLLECTIONURISYSTEM_TEAMPROJECT/_apis/build/builds/BUILD_BUILDID?api-version=5.1"
Assert-MockCalled -CommandName Invoke-RestMethod -Times 1 -Scope It -ParameterFilter {
# validate the paremters
if ($Uri -ne $expectedUri) {
return $False
}
if ($Headers.Authorization -ne ("Bearer {0}" -f $envVariables["ACCESSTOKEN"])) {
return $False
}
if ($Method -ne "PATCH") {
return $False
}
if ($ContentType -ne "application/json") {
return $False
}
# compare the payload
$bodyObj = ConvertFrom-Json $Body
if ($bodyObj.status -ne "Cancelling") {
return $False
}
return $True
}
}
It 'performs the rest method with an error' {
Mock Invoke-RestMethod {
throw [System.Exception]::("Test")
}
#set env vars
{ Stop-Pipeline } | Should -Throw
}
}
Context 'without an env var' {
BeforeAll {
$Script:envVariables = @{
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" = "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI";
"SYSTEM_TEAMPROJECT" = "SYSTEM_TEAMPROJECT";
"BUILD_BUILDID" = "BUILD_BUILDID";
"ACCESSTOKEN" = "ACCESSTOKEN"
}
$Script:envVariables.GetEnumerator() | ForEach-Object {
$key = $_.Key
Set-Item -Path "Env:$key" -Value $_.Value
Remove-Item -Path "Env:$key"
}
}
It 'fails calling the rest method' {
Mock Invoke-RestMethod {
return @{"status"=200;}
}
{ Stop-Pipeline } | Should -Throw
Assert-MockCalled -CommandName Invoke-RestMethod -Times 0 -Scope It
}
}
}
Describe 'Set-PipelineResult' {
Context 'with all the env vars present' {
BeforeAll {
$Script:envVariables = @{
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" = "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI";
"SYSTEM_TEAMPROJECT" = "SYSTEM_TEAMPROJECT";
"BUILD_BUILDID" = "BUILD_BUILDID";
"ACCESSTOKEN" = "ACCESSTOKEN"
}
$envVariables.GetEnumerator() | ForEach-Object {
$key = $_.Key
Set-Item -Path "Env:$key" -Value $_.Value
}
}
It 'performs the rest call' {
Mock Invoke-RestMethod {
return @{"status"=200;}
}
Set-PipelineResult "succeeded"
$expectedUri = "SYSTEM_TEAMFOUNDATIONCOLLECTIONURISYSTEM_TEAMPROJECT/_apis/build/builds/BUILD_BUILDID?api-version=5.1"
Assert-MockCalled -CommandName Invoke-RestMethod -Times 1 -Scope It -ParameterFilter {
# validate the paremters
if ($Uri -ne $expectedUri) {
return $False
}
if ($Headers.Authorization -ne ("Bearer {0}" -f $envVariables["ACCESSTOKEN"])) {
return $False
}
if ($Method -ne "PATCH") {
return $False
}
if ($ContentType -ne "application/json") {
return $False
}
# compare the payload
$bodyObj = ConvertFrom-Json $Body
if ($bodyObj.result -ne "succeeded") {
return $False
}
return $True
}
}
It 'performs the rest method with an error' {
Mock Invoke-RestMethod {
throw [System.Exception]::("Test")
}
#set env vars
{ Set-PipelineResult "failed" } | Should -Throw
}
}
Context 'without an env var' {
BeforeAll {
$Script:envVariables = @{
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" = "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI";
"SYSTEM_TEAMPROJECT" = "SYSTEM_TEAMPROJECT";
"BUILD_BUILDID" = "BUILD_BUILDID";
"ACCESSTOKEN" = "ACCESSTOKEN"
}
$Script:envVariables.GetEnumerator() | ForEach-Object {
$key = $_.Key
Set-Item -Path "Env:$key" -Value $_.Value
Remove-Item -Path "Env:$key"
}
}
It 'fails calling the rest method' {
Mock Invoke-RestMethod {
return @{"status"=200;}
}
{ Set-PipelineResult "failed" } | Should -Throw
Assert-MockCalled -CommandName Invoke-RestMethod -Times 0 -Scope It
}
}
}

Просмотреть файл

@ -0,0 +1,181 @@
<#
.SYNOPSIS
Returns the uri to be used for the VSTS rest API.
#>
function Get-BuildUrl {
$targetUrl = $Env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI + "$Env:SYSTEM_TEAMPROJECT/_apis/build/builds/" + $Env:BUILD_BUILDID + "?api-version=5.1"
return $targetUrl
}
<#
.SYNOPSIS
Returns the uri to be used for the VSTS rest API for tags.
#>
function Get-TagsRestAPIUrl {
param
(
[Parameter(Mandatory)]
[String]
$Tag
)
$targetUrl = $Env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI + "$Env:SYSTEM_TEAMPROJECT/_apis/build/builds/" + $Env:BUILD_BUILDID + "/tags/" + $Tag + "?api-version=6.0"
return $targetUrl
}
<#
.SYNOPSIS
Returns the auth heater to use with the REST API of VSTS.
#>
function Get-AuthHeader([string] $AccessToken)
{
# User name can be anything. It is the personal access token (PAT) token that matters.
$user = "AnyUser"
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user, $AccessToken)))
$headers = @{Authorization = "Basic {0}" -f $base64AuthInfo}
return $headers
}
<#
.SYNOPSIS
Cancels the pipeline and no other steps of job will be executed.
.EXAMPLE
Stop-Pipeline
.NOTES
The cmdlet depends on the following environment variables. If they are not present
an InvalidOperationException will be thrown.
* SYSTEM_TEAMFOUNDATIONCOLLECTIONURI: Contains the full uri of the VSTS for the team.
* SYSTEM_TEAMPROJECT: Contains the name of the team in VSTS.
* BUILD_BUILDID: The id of the build to cancel.
* ACCESSTOKEN: The PAT used to be able to perform the rest call to the VSTS API.
#>
function Stop-Pipeline {
# assert that all the env vars that are needed are present, else we do have an error
$envVars = @{
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" = $Env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
"SYSTEM_TEAMPROJECT" = $Env:SYSTEM_TEAMPROJECT;
"BUILD_BUILDID" = $Env:BUILD_BUILDID;
"ACCESSTOKEN" = $Env:ACCESSTOKEN
}
foreach ($key in $envVars.Keys) {
if (-not($envVars[$key])) {
Write-Debug "Environment variable missing: $key"
throw [System.InvalidOperationException]::new("Environment variable missing: $key")
}
}
$url = Get-BuildUrl
$headers = Get-AuthHeader -AccessToken $Env:ACCESSTOKEN
$payload = @{
status = "Cancelling"
}
return Invoke-RestMethod -Uri $url -Headers $headers -Method "PATCH" -Body ($payload | ConvertTo-json) -ContentType 'application/json'
}
<#
.SYNOPSIS
Allows to set the final status of the pipeline.
.EXAMPLE
Set-PipelineResult "failed"
.NOTES
The cmdlet depends on the following environment variables. If they are not present
an InvalidOperationException will be thrown.
* SYSTEM_TEAMFOUNDATIONCOLLECTIONURI: Contains the full uri of the VSTS for the team.
* SYSTEM_TEAMPROJECT: Contains the name of the team in VSTS.
* BUILD_BUILDID: The id of the build to cancel.
* ACCESSTOKEN: The PAT used to be able to perform the rest call to the VSTS API.
The valid values of status are:
* "canceled" The build was canceled before starting.
* "failed" The build completed unsuccessfully.
* "none" No result
* "partiallySucceeded" The build completed compilation successfully but had other errors.
* "succeeded" The build completed successfully.
#>
function Set-PipelineResult {
param
(
[Parameter(Mandatory)]
[String]
[ValidateScript({
$("canceled", "failed", "none", "partiallySucceeded", "succeeded").Contains($_) # validate that the status is in the range of valid values
})]
$Status
)
# assert that all the env vars that are needed are present, else we do have an error
$envVars = @{
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" = $Env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
"SYSTEM_TEAMPROJECT" = $Env:SYSTEM_TEAMPROJECT;
"BUILD_BUILDID" = $Env:BUILD_BUILDID;
"ACCESSTOKEN" = $Env:ACCESSTOKEN
}
foreach ($key in $envVars.Keys) {
if (-not($envVars[$key])) {
Write-Debug "Environment variable missing: $key"
throw [System.InvalidOperationException]::new("Environment variable missing: $key")
}
}
$url = Get-BuildUrl
$headers = Get-AuthHeader -AccessToken $Env:ACCESSTOKEN
$payload = @{
result = $Status
}
return Invoke-RestMethod -Uri $url -Headers $headers -Method "PATCH" -Body ($payload | ConvertTo-json) -ContentType 'application/json'
}
function Set-BuildTags {
param
(
[String[]]
$Tags
)
$envVars = @{
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" = $Env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
"SYSTEM_TEAMPROJECT" = $Env:SYSTEM_TEAMPROJECT;
"BUILD_BUILDID" = $Env:BUILD_BUILDID;
"ACCESSTOKEN" = $Env:ACCESSTOKEN
}
foreach ($key in $envVars.Keys) {
if (-not($envVars[$key])) {
Write-Debug "Environment variable missing: $key"
throw [System.InvalidOperationException]::new("Environment variable missing: $key")
}
}
# there is an api to just do one request, but it is not clear what should the body be, and we are trying and failing, ergo, use
# the API that sets one tag at at time.
# This is why people should write documentation, now I'm being annoying with the tags
$headers = Get-AuthHeader -AccessToken $Env:ACCESSTOKEN
foreach ($t in $Tags) {
$url = Get-TagsRestAPIUrl -Tag $t
Write-Host "Uri is $url"
Invoke-RestMethod -Uri $url -Headers $headers -Method "PUT" -ContentType 'application/json'
}
}
# export public functions, other functions are private and should not be used ouside the module.
Export-ModuleMember -Function Stop-Pipeline
Export-ModuleMember -Function Set-PipelineResult
Export-ModuleMember -Function Set-BuildTags

Просмотреть файл

@ -0,0 +1,20 @@
#!/bin/bash -ex
# env var should have been defined by the CI
if test -z "$XAM_TOP"; then
echo "Variable XAM_TOP is missing."
exit 1
fi
cd $XAM_TOP
DOTNET_NUPKG_DIR=$(make -C tools/devops print-abspath-variable VARIABLE=DOTNET_NUPKG_DIR | grep "^DOTNET_NUPKG_DIR=" | sed -e 's/^DOTNET_NUPKG_DIR=//')
mkdir -p ../package/
rm -f ../package/*.nupkg
cp -c "$DOTNET_NUPKG_DIR"/*.nupkg ../package/
DOTNET_PKG_DIR=$(make -C tools/devops print-abspath-variable VARIABLE=DOTNET_PKG_DIR | grep "^DOTNET_PKG_DIR=" | sed -e 's/^DOTNET_PKG_DIR=//')
make -C dotnet package -j
cp -c "$DOTNET_PKG_DIR"/*.pkg ../package/
cp -c "$DOTNET_PKG_DIR"/*.msi ../package/

Просмотреть файл

@ -0,0 +1,65 @@
#
# Selects appropriate agent pool based on trigger type (PR or CI)
#
parameters:
agentPoolPR: 'VSEng-Xamarin-RedmondMacCatalinaBuildPool-iOS-Untrusted'
agentPoolPRUrl: 'https://devdiv.visualstudio.com/DevDiv/_settings/agentqueues?queueId=2734&view=agents'
agentPoolCI: 'VSEng-Xamarin-RedmondMacCatalinaBuildPool-iOS-Trusted'
agentPoolCIUrl: 'https://devdiv.visualstudio.com/DevDiv/_settings/agentqueues?queueId=2748&view=agents'
condition: succeeded()
steps:
- powershell: |
$buildReason = "$(Build.Reason)"
$buildSourceBranchName = "$(Build.SourceBranchName)"
$agentPoolPR = "${{ parameters.agentPoolPR }}"
$agentPoolPRUrl = "${{ parameters.agentPoolPRUrl }}"
$agentPoolCI = "${{ parameters.agentPoolCI }}"
$agentPoolCIUrl = "${{ parameters.agentPoolCIUrl }}"
Write-Host "buildReason: ${buildReason}"
Write-Host "buildSourceBranchName: ${buildSourceBranchName}"
Write-Host "agentPoolPR: ${agentPoolPR}"
Write-Host "agentPoolPRUrl: ${agentPoolPRUrl}"
Write-Host "agentPoolCI: ${agentPoolCI}"
Write-Host "agentPoolCIUrl: ${agentPoolCIUrl}"
$agentPool = $agentPoolPR # Default to Catalina PR pool
$agentPoolUrl = $agentPoolPRUrl
Write-Host "Default agent pool: ${agentPool}"
[bool] $isTopicBranch = $False
[bool] $isPullRequest = $False
if (-not ($buildSourceBranchName -eq 'main' -or $buildSourceBranchName -eq 'master' -or $buildSourceBranchName.StartsWith('d16-'))) {
$isTopicBranch = $True
}
if ($buildReason -eq 'PullRequest') {
$prTargetBranchName = "$(System.PullRequest.TargetBranch)" # This system variable is only defined (and in turn the value macro replaced) when $buildReason is 'PullRequest'. Consequently, it cannot be defined as part of an input parameter
Write-Host "prTargetBranchName: System.PullRequest.TargetBranch: ${prTargetBranchName}"
$isPullRequest = $True
$targetBranch = $prTargetBranchName
} else {
$targetBranch = $buildSourceBranchName
}
Write-Host "Settings:"
Write-Host " targetBranch: ${targetBranch}"
Write-Host " isTopicBranch: ${isTopicBranch}"
Write-Host " isPullRequest: ${isPullRequest}"
if ($isTopicBranch -or $isPullRequest) {
$agentPool = $agentPoolPR # Untrusted on-prem iOS pool used for all PRs (including those from forks) and feature/topic branch commits not targeting main or d16-x branches
$agentPoolUrl = $agentPoolPRUrl
} else {
$agentPool = $agentPoolCI # Trusted on-prem iOS pool used for CIs targeting main and d16-x release branches
$agentPoolUrl = $agentPoolCIUrl
}
Write-Host "AgentPoolComputed: ${agentPool}"
Write-Host "Selected agent pool: ${agentPoolUrl}"
Write-Host "##vso[task.setvariable variable=AgentPoolComputed;isOutput=true]$agentPool"
name: setAgentPool
displayName: 'AgentPoolSelector: Select agent pool'
condition: ${{ parameters.condition }}

Просмотреть файл

@ -0,0 +1,42 @@
# common steps to download the artifacts from the test results.
parameters:
- name: devicePrefix
type: string
default: 'ios' # default context, since we started dealing with iOS devices.
steps:
- checkout: self
persistCredentials: true
# Download the Html Report that was added by the tests job.
- task: DownloadPipelineArtifact@2
displayName: Download html report
inputs:
patterns: 'HtmlReport-${{ parameters.devicePrefix }}/HtmlReport.zip'
allowFailedBuilds: true
path: $(System.DefaultWorkingDirectory)/Reports
# Unzip report.
- task: ExtractFiles@1
displayName: 'Extract HmlReport'
inputs:
archiveFilePatterns: '$(System.DefaultWorkingDirectory)/Reports/HtmlReport-${{ parameters.devicePrefix }}/HtmlReport.zip'
destinationFolder: '$(System.DefaultWorkingDirectory)/HtmlReport-${{ parameters.devicePrefix }}'
# Download the test report to write the comment.
- task: DownloadPipelineArtifact@2
displayName: Download Test Summary
inputs:
patterns: '**/TestSummary-${{ parameters.devicePrefix }}/TestSummary.md'
allowFailedBuilds: true
path: $(System.DefaultWorkingDirectory)\Reports
- powershell: |
Get-ChildItem -Recurse $Env:SYSTEM_DEFAULTWORKINGDIRECTORY
Write-Host "##vso[task.setvariable variable=TEST_SUMMARY_PATH]$Env:SYSTEM_DEFAULTWORKINGDIRECTORY\Reports\TestSummary-${{ parameters.devicePrefix }}\TestSummary.md"
Write-Host "##vso[task.setvariable variable=HTML_REPORT_PATH]$Env:SYSTEM_DEFAULTWORKINGDIRECTORY\HtmlReport-${{ parameters.devicePrefix }}"
displayName: Pusblish artifact paths
name: artifacts # not to be confused with the displayName, this is used to later use the name of the step to access the output variables from an other job

Просмотреть файл

@ -0,0 +1,58 @@
# Job that will download the other artifact from the tests job and will publish them in the
# vsdrops
###########################################################
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING #
###########################################################
# This job is executed on WINDOWS! make sure you DO NOT USE
# bash or linux file paths on scripts. Another important
# details is that System.DefaultWorkingDirectory
# on mac os x points on the top dir while on windows
# is the checked out dir
parameters:
- name: statusContext
type: string
default: 'iOS Device Tests' # default context, since we started dealing with iOS devices.
- name: vsdropsPrefix
type: string
- name: devicePrefix
type: string
default: 'ios' # default context, since we started dealing with iOS devices.
steps:
- checkout: self
persistCredentials: true
- template: download-artifacts.yml
parameters:
devicePrefix: ${{ parameters.devicePrefix }}
# Use the cmdlet to post a new summary comment. The cmdlet checks if we have the TestSummary.md file or not. It will also add the appropriate links to the comment.
# this step uses variables that have been set by the tests job dependency via output variables, those variables contain if the xamarin-storage could be used and its path
- powershell: |
$env:VSDROPS_INDEX="$Env:VSDROPSPREFIX/$Env:BUILD_BUILDNUMBER/$Env:BUILD_BUILDID/$Env:DEVICE_PREFIX/;/tests/vsdrops_index.html"
Import-Module $Env:SYSTEM_DEFAULTWORKINGDIRECTORY\xamarin-macios\tools\devops\automation\scripts\GitHub.psm1
Import-Module $Env:SYSTEM_DEFAULTWORKINGDIRECTORY\xamarin-macios\tools\devops\automation\scripts\VSTS.psm1
$response = New-GitHubSummaryComment -Context "$Env:CONTEXT" -TestSummaryPath "$Env:TESTS_SUMMARY"
Write-Host $response
if($Env:TESTS_JOBSTATUS -ne "Succeeded")
{
Set-PipelineResult -Status partiallySucceeded
}
env:
BUILD_REVISION: $(Build.SourceVersion)
CONTEXT: ${{ parameters.statusContext }}
DEVICE_PREFIX: ${{ parameters.devicePrefix }}
GITHUB_TOKEN: $(GitHub.Token)
TESTS_JOBSTATUS: $(TESTS_JOBSTATUS) # set by the runTests step
TESTS_SUMMARY: $(TEST_SUMMARY_PATH)
ACCESSTOKEN: $(System.AccessToken)
displayName: 'Add summaries'
condition: always()
timeoutInMinutes: 1

Просмотреть файл

@ -0,0 +1,27 @@
# job that downloads the html report from the artifacts and uploads them into vsdrops.
parameters:
- name: devicePrefix
type: string
default: 'ios' # default context, since we started dealing with iOS devices.
steps:
- checkout: self
persistCredentials: true
- template: download-artifacts.yml
parameters:
devicePrefix: ${{ parameters.devicePrefix }}
# Upload full report to vsdrops using the the build numer and id as uuids.
- task: ms-vscs-artifact.build-tasks.artifactDropTask-1.artifactDropTask@0
displayName: 'Publish to Artifact Services Drop'
inputs:
dropServiceURI: 'https://devdiv.artifacts.visualstudio.com/DefaultCollection'
dropMetadataContainerName: 'DropMetadata-${{ parameters.devicePrefix }}'
buildNumber: 'xamarin-macios/device-tests/$(Build.BuildNumber)/$(Build.BuildId)/${{ parameters.devicePrefix }}'
sourcePath: $(HTML_REPORT_PATH)
detailedLog: true
usePat: true

Просмотреть файл

@ -0,0 +1,25 @@
# imports the xml to the vsts test results for the job
parameters:
- name: devicePrefix
type: string
default: 'ios' # default context, since we started dealing with iOS devices.
steps:
- checkout: self
persistCredentials: true
- template: download-artifacts.yml
parameters:
devicePrefix: ${{ parameters.devicePrefix }}
# Upload test results to vsts.
- task: PublishTestResults@2
displayName: 'Publish NUnit Device Test Results'
inputs:
testResultsFormat: NUnit
testResultsFiles: '**/vsts-*.xml'
failTaskOnFailedTests: true
continueOnError: true
condition: succeededOrFailed()

Просмотреть файл

@ -0,0 +1,269 @@
# Xamarin
#
# Template that contains the different steps required to run device
# tests. The template takes a number of parameters so that it can
# be configured for the different type of devices.
#
parameters:
- name: statusContext
type: string
default: 'iOS Device Tests' # default context, since we started dealing with iOS devices.
- name: testsLabels
type: string
default: '--label=run-ios-64-tests,run-non-monotouch-tests,run-monotouch-tests,run-mscorlib-tests' # default context, since we started dealing with iOS devices.
- name: disableProvisionatorCache
type: boolean
default: false
- name: clearProvisionatorCache
type: boolean
default: false
- name: useXamarinStorage
type: boolean
default: false # xamarin-storage will disappear, so by default do not use it
- name: vsdropsPrefix
type: string
# can depend on the pool, which is annoying, but we should keep it in mind
- name: keyringPass
type: string
- name: devicePrefix
type: string
default: 'ios' # default context, since we started dealing with iOS devices.
steps:
- checkout: self
- checkout: maccore
persistCredentials: true # hugely important, else there are some scripts that check a single file from maccore that will fail
- bash: $(System.DefaultWorkingDirectory)/xamarin-macios/tools/devops/automation/scripts/bash/clean-bot.sh
displayName: 'Clean bot'
env:
BUILD_REVISION: 'jenkins'
continueOnError: true
- bash: |
security set-key-partition-list -S apple-tool:,apple: -s -k $KEYCHAIN_PASS login.keychain
env:
KEYCHAIN_PASS: ${{ parameters.keyringPass }}
displayName: 'Remove security UI-prompt (http://stackoverflow.com/a/40039594/183422)'
condition: succeededOrFailed() # we do not care about the previous process cleanup
continueOnError: true
- bash: cd $(System.DefaultWorkingDirectory)/xamarin-macios/ && git clean -xdf
displayName: 'Clean workspace'
# Run the pipeline script tests to ensure that we will have not have an unexpected behaviour.
- bash: make -C $(System.DefaultWorkingDirectory)/xamarin-macios/tools/devops/automation/scripts run-tests
displayName: 'Run pipeline script tests'
- pwsh : |
gci env: | format-table -autosize -wrap
displayName: 'Dump Environment'
# Use a cmdlet to check if the space available in the devices root system is larger than 50 gb. If there is not
# enough space available it:
# 1. Set the status of the build to error. It is not a failure since no tests have been ran.
# 2. Set a comment stating the same as what was sent to the status.
# 3. Cancel the pipeline and do not execute any of the following steps.
- pwsh: |
cd $Env:SYSTEM_DEFAULTWORKINGDIRECTORY/xamarin-macios/tools/devops/automation/scripts/
Import-Module ./System.psm1
Import-Module ./VSTS.psm1
Import-Module ./GitHub.psm1
if ( -not (Test-HDFreeSpace -Size 50)) {
Set-GitHubStatus -Status "error" -Description "Not enough free space in the host." -Context "$Env:CONTEXT"
New-GitHubComment -Header "Tests failed catastrophically on $Env:CONTEXT" -Emoji ":fire:" -Description "Not enough free space in the host."
Stop-Pipeline
} else {
Set-GitHubStatus -Status "pending" -Description "Device tests on VSTS have been started." -Context "$Env:CONTEXT"
}
env:
BUILD_REVISION: $(Build.SourceVersion)
CONTEXT: ${{ parameters.statusContext }}
GITHUB_TOKEN: $(GitHub.Token)
ACCESSTOKEN: $(System.AccessToken)
displayName: 'Check HD Free Space'
timeoutInMinutes: 5
condition: succeededOrFailed() # we do not care about the previous step
# if we got to this point, it means that we do have at least 50 Gb to run the test, should
# be more than enough, else the above script would have stopped the pipeline
- bash: |
set -x
set -e
cd xamarin-macios
./configure --enable-xamarin
displayName: 'Enable Xamarin'
timeoutInMinutes: 1
# Add the required provisioning profiles to be able to execute the tests.
- bash: |
set -x
set -e
rm -f ~/Library/Caches/com.xamarin.provisionator/Provisions/*p12
rm -f ~/Library/Caches/com.xamarin.provisionator/Provisions/*mobileprovision
./maccore/tools/install-qa-provisioning-profiles.sh -v
displayName: 'Add provisioning profiles'
timeoutInMinutes: 30
env:
LOGIN_KEYCHAIN_PASSWORD: ${{ parameters.keyringPass }}
# download the artifacts.json, which will use to find the URI of the built pkg to later be installed by provisionator
- task: DownloadPipelineArtifact@2
displayName: Download artifacts.json
inputs:
patterns: '**/*.json'
allowFailedBuilds: true
path: $(Build.SourcesDirectory)/artifacts
- pwsh: |
Dir $(Build.SourcesDirectory)/artifacts
$json = Get-Content '$(Build.SourcesDirectory)/artifacts/pkg-info/artifacts.json' | Out-String | ConvertFrom-Json
foreach ($i in $json) {
if ($i.tag -like "xamarin-ios*" -and -not ($i.url -like "*notarized*")) {
$url = $i.url
Write-Host "##vso[task.setvariable variable=XI_PACKAGE;]$url"
break
}
}
displayName: 'Set iOS pkgs url'
timeoutInMinutes: 5
- bash: |
echo "Pkg uri is $XI_PACKAGE"
make -C $(System.DefaultWorkingDirectory)/xamarin-macios/tools/devops/ device-tests-provisioning.csx
displayName: 'Generate Provisionator csx file'
# Executed ONLY if we want to clear the provisionator cache.
- bash: rm -rf "$TOOLS_DIR/provisionator"
env:
TOOLS_DIR: $(Agent.ToolsDirectory)
displayName: 'Nuke Provisionator Tool Cache'
condition: ${{ parameters.clearProvisionatorCache }}
# Use the provisionator to install the test dependencies. Those have been generated in the 'Generate Provisionator csx file' step.
- task: xamops.azdevex.provisionator-task.provisionator@1
displayName: 'Provision dependencies'
inputs:
provisioning_script: $(System.DefaultWorkingDirectory)/xamarin-macios/tools/devops/device-tests-provisioning.csx
provisioning_extra_args: '-vvvv'
timeoutInMinutes: 250
# remove any old processes that might have been left behind.
- pwsh : |
Import-Module $Env:SYSTEM_DEFAULTWORKINGDIRECTORY/xamarin-macios/tools/devops/automation/scripts/System.psm1
Clear-XamarinProcesses
displayName: 'Process cleanup'
# Increase mlaunch verbosity. Will step on the old setting present.
- pwsh : |
Import-Module $Env:SYSTEM_DEFAULTWORKINGDIRECTORY/xamarin-macios/tools/devops/automation/scripts/MLaunch.psm1
Set-MLaunchVerbosity -Verbosity 10
displayName: 'Make mlaunch verbose'
condition: succeededOrFailed() # we do not care about the previous step
# Re-start the daemon used to find the devices in the bot.
- pwsh : |
Import-Module $Env:SYSTEM_DEFAULTWORKINGDIRECTORY/xamarin-macios/tools/devops/automation/scripts/MLaunch.psm1
Optimize-DeviceDiscovery
displayName: 'Fix device discovery (reset launchctl)'
condition: succeededOrFailed() # making mlaunch verbose should be a non blocker
# Update the status to pending, that way the monitoring person knows that we started running the tests. Up to this
# point we were just setting up the agent.
- pwsh: |
Import-Module $Env:SYSTEM_DEFAULTWORKINGDIRECTORY/xamarin-macios/tools/devops/automation/scripts/GitHub.psm1
Set-GitHubStatus -Status "pending" -Context "$Env:CONTEXT" -Description "Running device tests on $Env:CONTEXT"
env:
BUILD_REVISION: $(Build.SourceVersion)
CONTEXT: ${{ parameters.statusContext }}
GITHUB_TOKEN: $(GitHub.Token)
displayName: Set pending GitHub status
continueOnError: true
condition: succeededOrFailed() # re-starting the daemon should not be an issue
timeoutInMinutes: 5
# Run tests. If we are using xamarin-storage add a periodic command to be executed by xharness, else, since we are using vsdrops do nothing.
- bash: |
set -x
set -e
cd $WORKING_DIR/xamarin-macios
echo "Running tests on $AGENT_NAME"
echo "##vso[task.setvariable variable=TESTS_BOT;isOutput=true]$AGENT_NAME"
make -C builds download -j || true
make -C builds downloads -j || true
make -C builds .stamp-mono-ios-sdk-destdir -j || true
EC=0
MONO_ENV_OPTIONS=--trace=E:all make -C tests vsts-device-tests || EC=$?
if [ $EC -eq 0 ]; then
echo '##vso[task.setvariable variable=TESTS_JOBSTATUS;isOutput=true]Succeeded'
else
echo '##vso[task.setvariable variable=TESTS_JOBSTATUS;isOutput=true]Failed'
fi
env:
WORKING_DIR: $(System.DefaultWorkingDirectory)
TESTS_EXTRA_ARGUMENTS: ${{ parameters.testsLabels }}
USE_XAMARIN_STORAGE: ${{ parameters.useXamarinStorage }}
VSDROPS_URI: '${{ parameters.vsdropsPrefix }}/$(Build.BuildNumber)/$(Build.BuildId);/tests/' # uri used to create the vsdrops index using full uri
USE_TCP_TUNNEL: 'true'
displayName: 'Run tests'
name: runTests # not to be confused with the displayName, this is used to later use the name of the step to access the output variables from an other job
timeoutInMinutes: 600
# Upload TestSummary as an artifact.
- task: PublishPipelineArtifact@1
displayName: 'Publish Artifact: TestSummary'
inputs:
targetPath: 'xamarin-macios/tests/TestSummary.md'
artifactName: TestSummary-${{ parameters.devicePrefix }}
continueOnError: true
condition: succeededOrFailed()
- pwsh: |
$summaryName = "TestSummary-$Env:PREFIX.md"
$summaryPath = "$Env:SYSTEM_DEFAULTWORKINGDIRECTORY/xamarin-macios/tests/TestSummary.md"
Write-Host "##vso[task.addattachment type=Distributedtask.Core.Summary;name=$summaryName;]$summaryPath"
displayName: Set TestSummary
env:
PREFIX: ${{ parameters.devicePrefix }}
# Archive files for the Html Report so that the report can be easily uploaded as artifacts of the build.
- task: ArchiveFiles@1
displayName: 'Archive HtmlReport'
inputs:
rootFolder: 'xamarin-macios/jenkins-results'
includeRootFolder: false
archiveFile: '$(Build.ArtifactStagingDirectory)/HtmlReport.zip'
continueOnError: true
condition: succeededOrFailed()
# Create HtmlReport artifact. This serves two purposes:
# 1. It is the way we are going to share the HtmlReport with the publish_html job that is executed on a Windows machine.
# 2. Users can download this if they want.
- task: PublishPipelineArtifact@1
displayName: 'Publish Artifact: HtmlReport'
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)/HtmlReport.zip'
artifactName: HtmlReport-${{ parameters.devicePrefix }}
continueOnError: true
condition: succeededOrFailed()
# Be nice and clean behind you
- pwsh: |
cd $Env:SYSTEM_DEFAULTWORKINGDIRECTORY/xamarin-macios/tools/devops/automation/scripts/
Import-Module ./System.psm1
Clear-AfterTests
displayName: 'Cleanup'
continueOnError: true
condition: always() # no matter what, includes cancellation

Просмотреть файл

@ -0,0 +1,128 @@
# Main template that contains all the jobs that are required to run the device tests.
#
# The stage contains two different jobs
#
# tests: Runs the tests on a pool that contains devices that are capable to run them.
# publish_html: Because vsdrop is not supported on macOS we have an extra job that
# will run on a pool with Windows devices that will publish the results on VSDrop to
# be browsable.
parameters:
# string that is used to identify the status to be used to expose the result on GitHub
- name: statusContext
type: string
default: 'iOS Device Tests' # default context, since we started dealing with iOS devices.
# string that contains the extra labels to pass to xharness to select the tests to execute.
- name: testsLabels
type: string
default: '--label=run-ios-64-tests,run-non-monotouch-tests,run-monotouch-tests,run-mscorlib-tests' # default context, since we started dealing with iOS devices.
# name of the pool that contains the iOS devices
- name: iOSDevicePool
type: string
default: 'VSEng-Xamarin-QA'
# demand that has to be matched by a bot to be able to run the tests.
- name: iOSDeviceDemand
type: string
default: 'xismoke'
- name: useXamarinStorage
type: boolean
default: false
- name: vsdropsPrefix
type: string
- name: stageName
type: string
- name: keyringPass
type: string
- name: execute
type: string
- name: devicePrefix
type: string
default: 'ios' # default context, since we started dealing with iOS devices.
stages:
- stage:
displayName: ${{ parameters.stageName }}
dependsOn:
- build_packages
# we need to have the pkgs built and the device sets to be ran, that is decided via the labels or type of build during the build_packages stage
condition: and(succeeded(), eq(dependencies.build_packages.outputs['build.configuration.RunDeviceTests'], 'True'))
jobs:
- job: tests
displayName: 'Run ${{ parameters.devicePrefix }} Device Tests'
timeoutInMinutes: 1000
pool:
name: ${{ parameters.iOSDevicePool }}
demands: ${{ parameters.iOSDeviceDemand }}
workspace:
clean: all
steps:
- template: build.yml
parameters:
testsLabels: ${{ parameters.testsLabels }}
statusContext: ${{ parameters.statusContext }}
useXamarinStorage: ${{ parameters.useXamarinStorage }}
vsdropsPrefix: ${{ parameters.vsdropsPrefix }}
keyringPass: ${{ parameters.keyringPass }}
devicePrefix: ${{ parameters.devicePrefix }}
- job: upload_vsdrops
displayName: 'Upload report to vsdrops'
timeoutInMinutes: 1000
dependsOn: tests # can start as soon as the tests are done
condition: succeededOrFailed()
pool:
vmImage: 'windows-latest'
workspace:
clean: all
steps:
- template: ../common/upload-vsdrops.yml
parameters:
devicePrefix: ${{ parameters.devicePrefix }}
- job: upload_vsts_tests
displayName: 'Upload xml to vsts'
timeoutInMinutes: 1000
dependsOn: tests # can start as soon as the tests are done
condition: succeededOrFailed()
pool:
vmImage: 'windows-latest'
workspace:
clean: all
steps:
- template: ../common/upload-vsts-tests.yml
parameters:
devicePrefix: ${{ parameters.devicePrefix }}
- job: publish_html
displayName: 'Publish Html report in VSDrops'
timeoutInMinutes: 1000
dependsOn: # has to wait for the tests to be done AND the data to be uploaded
- tests
- upload_vsdrops
- upload_vsts_tests
condition: succeededOrFailed()
variables:
# Define the variable FOO from the previous job
# Note the use of single quotes!
TESTS_BOT: $[ dependencies.tests.outputs['runTests.TESTS_BOT'] ]
TESTS_JOBSTATUS: $[ dependencies.tests.outputs['runTests.TESTS_JOBSTATUS'] ]
pool:
vmImage: 'windows-latest'
workspace:
clean: all
steps:
- template: ../common/publish-html.yml
parameters:
statusContext: ${{ parameters.statusContext }}
vsdropsPrefix: ${{ parameters.vsdropsPrefix }}
devicePrefix: ${{ parameters.devicePrefix }}

Просмотреть файл

@ -0,0 +1,67 @@
steps:
- checkout: self
- checkout: maccore
persistCredentials: true # hugely important, else there are some scripts that check a single file from maccore that will fail
- task: CredScan@3
displayName: "Run CredScan"
inputs:
suppressionsFile: '$(System.DefaultWorkingDirectory)/maccore/tools/devops/CredScanSuppressions.json'
outputFormat: 'sarif'
verboseOutput: true
- powershell: |
Write-Host 'Source dir $(Build.SourcesDirectory)'
Write-Host 'Working dir $System.DefaultWorkingDirectory)'
Dir $(Build.SourcesDirectory)
Dir $(System.DefaultWorkingDirectory)
displayName: Dump enviroment
- powershell: |
gci env: | format-table -autosize -wrap
displayName: 'Dump Environment'
- task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0
displayName: 'Component Detection'
- task: PoliCheck@1
inputs:
inputType: 'Basic'
targetType: 'F'
targetArgument: '$(Build.SourcesDirectory)'
result: 'PoliCheck.xml'
optionsUEPATH: '$(System.DefaultWorkingDirectory)/maccore/tools/devops/PoliCheckExclusions.xml'
- task: securedevelopmentteam.vss-secure-development-tools.build-task-postanalysis.PostAnalysis@1
displayName: 'Post Analysis'
inputs:
CredScan: true
PoliCheck: true
- task: WhiteSource Bolt@20
displayName: "WhiteSource Bolt analysis"
inputs:
cwd: $(System.DefaultWorkingDirectory)
- powershell: echo "##vso[task.setvariable variable=CHECKS_FAILED]True"
condition: failed() # we failed running the tests, therefore stop the pipeline
- powershell: |
Import-Module "$(System.DefaultWorkingDirectory)\xamarin-macios\tools\devops\automation\scripts\GitHub.psm1"
Import-Module "$(System.DefaultWorkingDirectory)\xamarin-macios\tools\devops\automation\scripts\VSTS.psm1"
$context = "Governance"
Write-Host "Checks failed: '$Env:CHECKS_FAILED'"
if ($Env:CHECKS_FAILED -eq "True") {
Set-GitHubStatus -Status "error" -Description "Governance checks failed" -Context "$context"
} else {
Set-GitHubStatus -Status "success" -Description "Governance checks passed" -Context "$context"
}
env:
BUILD_REVISION: $(Build.SourceVersion)
GITHUB_TOKEN: $(GitHub.Token)
ACCESSTOKEN: $(System.AccessToken)
displayName: "Set Github status"
condition: succeededOrFailed()

Просмотреть файл

@ -0,0 +1,51 @@
parameters:
- name: macPool
type: string
- name: stageName
type: string
stages:
- stage:
displayName: ${{ parameters.stageName }}
dependsOn:
- build_packages
# we need to have the pkgs built and the device sets to be ran, that is decided via the labels or type of build during the build_packages stage
condition: and(succeeded(), eq (stageDependencies.build_packages.build.outputs['configuration.RunMacTests'], 'True'))
jobs:
- job: run_tests
displayName: 'Mac OS X tests'
timeoutInMinutes: 1000
workspace:
clean: all
pool:
name: ${{ parameters.macPool }}
demands:
- Agent.OS -equals Darwin
steps:
- checkout: self # https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=azure-devops&tabs=schema#checkout
clean: true # Executes: git clean -ffdx && git reset --hard HEAD
submodules: recursive
- bash: echo "Hello Tests"
displayName: 'So many job'
- job: upload_vsdrops
displayName: 'Upload results to vsdrops'
dependsOn: run_tests
timeoutInMinutes: 1000
workspace:
clean: all
pool:
name: VSEng-Xamarin-Win-XMA
demands:
- Agent.OS -equals Windows_NT
steps:
- checkout: self # https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=azure-devops&tabs=schema#checkout
clean: true # Executes: git clean -ffdx && git reset --hard HEAD
submodules: recursive

Просмотреть файл

@ -0,0 +1,440 @@
parameters:
- name: runTests
type: boolean
default: true
- name: runDeviceTests
type: boolean
default: true
- name: vsdropsPrefix
type: string
steps:
- checkout: self # https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=azure-devops&tabs=schema#checkout
clean: true # Executes: git clean -ffdx && git reset --hard HEAD
submodules: recursive
- checkout: maccore
clean: true
persistCredentials: true # hugely important, else there are some scripts that check a single file from maccore that will fail
- checkout: templates
clean: true
- checkout: release-scripts
clean: true
- powershell: |
gci env: | format-table -autosize -wrap
displayName: 'Dump Environment'
- bash: $(System.DefaultWorkingDirectory)/xamarin-macios/tools/devops/automation/scripts/bash/clean-bot.sh
displayName: 'Clean bot'
env:
BUILD_REVISION: 'jenkins'
continueOnError: true
- bash: |
security set-key-partition-list -S apple-tool:,apple: -s -k $OSX_KEYCHAIN_PASS login.keychain
env:
OSX_KEYCHAIN_PASS: $(OSX_KEYCHAIN_PASS)
displayName: 'Remove security UI-prompt (http://stackoverflow.com/a/40039594/183422)'
condition: succeededOrFailed() # we do not care about the previous process cleanup
- template: install-certificates.yml@templates
parameters:
DeveloperIdApplication: $(developer-id-application)
DeveloperIdInstaller: $(developer-id-installer)
IphoneDeveloper: $(iphone-developer)
MacDeveloper: $(mac-developer)
HostedMacKeychainPassword: $(OSX_KEYCHAIN_PASS)
- task: xamops.azdevex.provisionator-task.provisionator@2
displayName: 'Provision Brew components'
inputs:
provisioning_script: $(Build.SourcesDirectory)/xamarin-macios/tools/devops/provision-brew-packages.csx
provisioning_extra_args: '-vvvv'
timeoutInMinutes: 30
enabled: false
- bash: |
make -C $(Build.SourcesDirectory)/xamarin-macios/tools/devops build-provisioning.csx
displayName: 'Generate provisionator files.'
- task: xamops.azdevex.provisionator-task.provisionator@1
displayName: 'Provision Products & Frameworks'
inputs:
provisioning_script: $(Build.SourcesDirectory)/xamarin-macios/tools/devops/build-provisioning.csx
provisioning_extra_args: '-vvvv'
timeoutInMinutes: 250
- bash: |
set -x
sudo rm -Rf /Developer/MonoTouch
sudo rm -Rf /Library/Frameworks/Xamarin.iOS.framework
sudo rm -Rf /Library/Frameworks/Xamarin.Mac.framework
displayName: 'Delete library folders'
timeoutInMinutes: 5
- bash:
set -x
set -e
rm -Rvf $(Build.SourcesDirectory)/package
time make -C $(Build.SourcesDirectory)/xamarin-macios/ git-clean-all
displayName: 'Clear results directory'
timeoutInMinutes: 5
# Use the env variables that were set by the label parsing in the configure step
# print some useful logging to allow to know what is going on AND allow make some
# choices, there are labels that contradict each other (skip-package vs build-packages)
# we use warnings for those case we are not sure about.
- pwsh: |
# we have a number of scripts that require to be executed from the top of the src, rather
# than keeping track of the location of the script, we create two env vars that can be used to
# get to the top
$xamTop = "$(Build.SourcesDirectory)/xamarin-macios/"
Write-Host "##vso[task.setvariable variable=XAM_TOP]$xamTop"
$maccoreTop = "$(Build.SourcesDirectory)/maccore/"
Write-Host "##vso[task.setvariable variable=MACCORE_TOP]$maccoreTop"
$buildReason = "$(Build.Reason)"
$buildSourceBranchName = "$(Build.SourceBranchName)"
# decide if we are dealing with a PR or a re-triggered PR or a build from
# a branch in origin
if ($buildReason -eq "PullRequest" -or (($buildReason -eq "Manual") -and ($buildSourceBranchName -eq "merge")) ) {
Write-Host '##vso[task.setvariable variable=IsPR;isOutput=true]False'
if ($Env:BuildPackage -eq "True") {
Write-Host '##vso[task.setvariable variable=BuildPkgs;isOutput=true]True'
} else {
Write-Host '##vso[task.setvariable variable=BuildPkgs;isOutput=true]False'
}
# interesting case, we have build-pkg and skip-pkg... if that is the case, we build it, but we set a warning
if ($Env:BuildPackage -eq "True" -and $Env:SkipPackages -eq "True") {
Write-Host "##vso[task.logissue type=warning]'build-package' and 'skip-packages' are both present. Building packages in case of a doubt."
Write-Host "##vso[task.setvariable variable=BuildPkgs;isOutput=true]True"
}
# if we want to have device tests, we do need the pkgs so that we can fwd them to the device tests
if ($Env:TriggerDeviceTests -eq "True") {
Write-Host "##vso[task.setvariable variable=BuildPkgs;isOutput=true]True"
Write-Host "##vso[task.setvariable variable=RunDeviceTests;isOutput=true]True"
}
if ($Env:SkipNugets -eq "True") {
Write-Host "##vso[task.setvariable variable=BuildNugets;isOutput=true]False"
} else {
Write-Host "##vso[task.setvariable variable=BuildNugets;isOutput=true]True"
}
if ($Env:SkipSigning -eq "True") {
Write-Host "##vso[task.setvariable variable=SignPkgs;isOutput=true]False"
} else {
Write-Host "##vso[task.setvariable variable=SignPkgs;isOutput=true]True"
}
if ($Env:SkipExternalTests -eq "True") {
Write-Host "##vso[task.setvariable variable=RunExternalTests;isOutput=true]False"
} else {
Write-Host "##vso[task.setvariable variable=RunExternalTests;isOutput=true]True"
}
if ($Env:SkipPackagedXamarinMacTests -eq "True") {
Write-Host "##vso[task.setvariable variable=RunMacTests;isOutput=true]False"
} else {
Write-Host "##vso[task.setvariable variable=RunMacTests;isOutput=true]True"
}
if ($Env:SkipPublicJenkins -eq "True") {
Write-Host "##vso[task.setvariable variable=SkipPublicJenkins;isOutput=true]True"
} else {
Write-Host "##vso[task.setvariable variable=SkipPublicJenkins;isOutput=true]False"
}
Write-Host "##vso[task.setvariable variable=RunSampleTests;isOutput=true]$Env:RunSampleTests"
Write-Host "##vso[task.setvariable variable=RunInternalTests;isOutput=true]$Env:RunInternalTests"
} else {
# set the defaults, all the things! o/
Write-Host "##vso[task.setvariable variable=IsPR;isOutput=true]False"
# build pkg, nugets and sign them
Write-Host "##vso[task.setvariable variable=BuildPkgs;isOutput=true]True"
Write-Host "##vso[task.setvariable variable=BuildNugets;isOutput=true]True"
Write-Host "##vso[task.setvariable variable=SignPkgs;isOutput=true]True"
# tests, run all of them, internal, external, mac but not sample tests
Write-Host "##vso[task.setvariable variable=RunInternalTests;isOutput=true]True"
Write-Host "##vso[task.setvariable variable=RunExternalTests;isOutput=true]True"
Write-Host "##vso[task.setvariable variable=RunMacTests;isOutput=true]True"
Write-Host "##vso[task.setvariable variable=RunSampleTests;isOutput=true]False"
Write-Host "##vso[task.setvariable variable=SkipPublicJenkins;isOutput=true]False"
# if a developer decided to trigger one without device tests, allow it
if ($Env:RUN_DEVICE_TESTS -eq "true") {
Write-Host "##vso[task.setvariable variable=RunDeviceTests;isOutput=true]True"
} else {
Write-Host "##vso[task.setvariable variable=RunDeviceTests;isOutput=true]False"
}
}
name: configuration
displayName: "Parse PR labels"
timeoutInMinutes: 5
env:
RUN_DEVICE_TESTS: '${{ parameters.runDeviceTests }}'
- bash: |
set -x
set -e
if [[ "$IsPR" == "True" ]]; then
echo "Xamarin private packages NOT configured. Building a PR."
CONFIGURE_FLAGS=""
else
echo "Xamarin private packages configured."
CONFIGURE_FLAGS="--enable-xamarin"
fi
CONFIGURE_FLAGS="$CONFIGURE_FLAGS --enable-dotnet --enable-install-source"
cd $(Build.SourcesDirectory)/xamarin-macios/
./configure $CONFIGURE_FLAGS
echo $(cat $(Build.SourcesDirectory)/xamarin-macios/configure.inc)
env:
IsPR: $(configuration.IsPR)
displayName: "Configure build"
timeoutInMinutes: 5
# Actual build of the project
- bash: |
set -x
set -e
time make -C $(Build.SourcesDirectory)/xamarin-macios/ reset
time make -C $(Build.SourcesDirectory)/xamarin-macios/ all -j8
time make -C $(Build.SourcesDirectory)/xamarin-macios/ install -j8
displayName: 'Build'
timeoutInMinutes: 180
# build not signed .pkgs for the SDK
- bash: |
set -x
set -e
rm -Rf $(Build.SourcesDirectory)/package/*.pkg
rm -Rf $(Build.SourcesDirectory)/package/notarized/*.pkg
time make -C $(Build.SourcesDirectory)/xamarin-macios/ package
# output vars for other steps to use and not need to recomputed the paths
IOS_PKG=$(find $(Build.SourcesDirectory)/package -type f -name "xamarin.ios-*" | xargs basename)
if [ -z "$IOS_PKG" ]; then
echo "Xamarin.iOS package not found."
else
IOS_PKG="$(Build.SourcesDirectory)/package/$IOS_PKG"
echo "##vso[task.setvariable variable=IOS_PKG;]$IOS_PKG"
echo "Xamarin.iOS package found at $IOS_PKG"
fi
MAC_PKG=$(find $(Build.SourcesDirectory)/package -type f -name "xamarin.mac-*" | xargs basename)
if [ -z "$MAC_PKG" ]; then
echo "Xamarin.Mac package not found."
else
MAC_PKG="$(Build.SourcesDirectory)/package/$MAC_PKG"
echo "##vso[task.setvariable variable=MAC_PKG;]$MAC_PKG"
echo "Xamarin.Mac package found at $MAC_PKG"
fi
name: packages
displayName: 'Build Packages'
condition: and(succeeded(), contains(variables['configuration.BuildPkgs'], 'True'))
timeoutInMinutes: 180
# build nugets
- bash: $(Build.SourcesDirectory)/xamarin-macios/tools/devops/automation/scripts/bash/build-nugets.sh
displayName: 'Build Nugets'
condition: and(succeeded(), contains(variables['configuration.BuildNugets'], 'True'))
continueOnError: true # should not stop the build since is not official just yet.
timeoutInMinutes: 180
- bash: $(Build.SourcesDirectory)/xamarin-macios/tools/devops/automation/scripts/bash/productsign.sh
env:
PRODUCTSIGN_KEYCHAIN_PASSWORD: $(xma-password)
displayName: 'Signing PR Build'
condition: and(succeeded(), contains(variables['configuration.SignPkgs'], 'True'), contains(variables['configuration.IsPr'], 'True'))
# Ensure virtualenv is on the PATH
- template: set-path/v1.yml@templates
parameters:
prependToPath: '/Users/builder/Library/Python/2.7/bin'
- bash: |
VIRTUAL_ENV_PATH=$(Build.SourcesDirectory)/venv
pip install virtualenv
virtualenv "$VIRTUAL_ENV_PATH" --system-site-packages
source "$VIRTUAL_ENV_PATH/bin/activate"
pip install python-magic
security unlock-keychain -p $PRODUCTSIGN_KEYCHAIN_PASSWORD builder.keychain
PACKAGES="$IOS_PKG $MAC_PKG"
echo "Packages found at $PACKAGES"
echo "$PACKAGES" | xargs python $(Build.SourcesDirectory)/release-scripts/sign_and_notarize.py -a "$APP_ID" -i "$INSTALL_ID" -u "$APPLE_ACCOUNT" -p "$APPLE_PASS" -t "$TEAM_ID" -d $(Build.SourcesDirectory)/package/notarized -e "$MAC_ENTITLEMENTS" -k "$KEYCHAIN"
deactivate
rm -Rf "$VIRTUAL_ENV_PATH"
env:
PRODUCTSIGN_KEYCHAIN_PASSWORD: $(OSX_KEYCHAIN_PASS)
MAC_ENTITLEMENTS: $(Build.SourcesDirectory)/xamarin-macios/mac-entitlements.plist
APP_ID: $(XamarinAppId)
INSTALL_ID: $(XamarinAppId)
APPLE_ACCOUNT: $(XamarinUserId)
APPLE_PASS: $(XamarinPassword)
TEAM_ID: $(TeamID)
KEYCHAIN: $(SigningKeychain)
name: notarize
displayName: 'Signing Release Build'
condition: and(succeeded(), contains(variables['configuration.SignPkgs'], 'True'), contains(variables['configuration.IsPr'], 'False'))
timeoutInMinutes: 90
- template: generate-workspace-info.yml@templates
parameters:
GitHubToken: $(GitHub.Token)
ArtifactDirectory: $(Build.SourcesDirectory)/package-internal
- template: uninstall-certificates/v1.yml@templates
parameters:
HostedMacKeychainPassword: $(OSX_KEYCHAIN_PASS)
# upload each of the pkgs into the pipeline artifacts
- task: PublishPipelineArtifact@1
displayName: 'Publish Build Artifacts'
inputs:
targetPath: $(Build.SourcesDirectory)/package
artifactName: package
continueOnError: true
- task: PublishPipelineArtifact@1
displayName: 'Publish Build Internal Artifacts'
inputs:
targetPath: $(Build.SourcesDirectory)/package-internal
artifactName: package-internal
continueOnError: true
- bash: |
set -x
set -e
make -C $(Build.SourcesDirectory)/xamarin-macios/tests package-tests
displayName: 'Package Xamarin.mac tests'
condition: and(succeeded(), contains(variables['configuration.RunMacTests'], 'True'))
continueOnError: true # not a terrible blocking issue
- task: PublishPipelineArtifact@1
displayName: 'Publish Xamarin.Mac tests'
inputs:
targetPath: $(Build.SourcesDirectory)/xamarin-macios/tests/*.7z
artifactName: package-internal
condition: and(succeeded(), contains(variables['configuration.RunMacTests'], 'True'))
continueOnError: true
- bash: |
make -j8 -C $(Build.SourcesDirectory)/xamarin-macios/tools/apidiff jenkins-api-diff
# remove some files that do not need to be uploaded
cd $(Build.SourcesDirectory)/xamarin-macios/tools/apidiff/
rm -Rf *.exe *.pdb *.stamp *.zip *.sh ./references ./temp
displayName: 'API diff (from stable)'
condition: and(succeeded(), contains(variables['configuration.SkipPublicJenkins'], 'False'))
continueOnError: true
env:
BUILD_REVISION: 'jenkins'
- task: ArchiveFiles@1
displayName: 'Archive API diff (from stable)'
inputs:
rootFolder: $(Build.SourcesDirectory)/xamarin-macios/tools/apidiff
includeRootFolder: false
archiveFile: '$(Build.ArtifactStagingDirectory)/apidiff-stable.zip'
condition: and(succeeded(), contains(variables['configuration.SkipPublicJenkins'], 'False'))
continueOnError: true
- task: PublishPipelineArtifact@1
displayName: 'Publish API diff (from stable)'
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)/apidiff-stable.zip'
artifactName: apidiff-stable
condition: and(succeeded(), contains(variables['configuration.SkipPublicJenkins'], 'False'))
continueOnError: true
- bash: |
set -x
set -e
echo "Running tests on $AGENT_NAME"
echo "##vso[task.setvariable variable=TESTS_BOT;isOutput=true]$AGENT_NAME"
echo "##vso[task.setvariable variable=TESTS_RAN;isOutput=true]True"
rm -rf ~/.config/.mono/keypairs/
RC=0
make -C $(Build.SourcesDirectory)/xamarin-macios/tests "$TARGET" || RC=$?
if [ $RC -eq 0 ]; then
echo "##vso[task.setvariable variable=TESTS_JOBSTATUS;isOutput=true]Succeeded"
else
echo "##vso[task.setvariable variable=TESTS_JOBSTATUS;isOutput=true]Failed"
fi
if test -f "$(Build.SourcesDirectory)/xamarin-macios//jenkins/failure-stamp"; then
echo "Something went wrong:"
cat "$(Build.SourcesDirectory)/xamarin-macios//jenkins/pr-comments.md"
exit 1
fi
displayName: 'Run tests'
name: runTests # not to be confused with the displayName, this is used to later use the name of the step to access the output variables from an other job
timeoutInMinutes: 600
condition: succeededOrFailed() # we do not care about the previous process cleanup
enabled: ${{ parameters.runTests }}
env:
BUILD_REVISION: jenkins
TARGET: 'wrench-jenkins'
VSDROPS_URI: '${{ parameters.vsdropsPrefix }}/$(Build.BuildNumber)/$(Build.BuildId);/tests/' # uri used to create the vsdrops index using full uri
# Upload TestSummary as an artifact.
- task: PublishPipelineArtifact@1
displayName: 'Publish Artifact: TestSummary'
inputs:
targetPath: 'xamarin-macios/tests/TestSummary.md'
artifactName: TestSummary-sim
continueOnError: true
condition: and(succeededOrFailed(), contains(variables['runTests.TESTS_RAN'], 'True')) # if tests did not run, there is nothing to do
- pwsh: |
$summaryName = "TestSummary.md"
$summaryPath = "$Env:SYSTEM_DEFAULTWORKINGDIRECTORY/xamarin-macios/tests/TestSummary.md"
Write-Host "##vso[task.addattachment type=Distributedtask.Core.Summary;name=$summaryName;]$summaryPath"
displayName: Set TestSummary
condition: and(succeededOrFailed(), contains(variables['runTests.TESTS_RAN'], 'True')) # if tests did not run, there is nothing to do
# Archive files for the Html Report so that the report can be easily uploaded as artifacts of the build.
- task: ArchiveFiles@1
displayName: 'Archive HtmlReport'
inputs:
rootFolder: 'xamarin-macios/jenkins-results'
includeRootFolder: false
archiveFile: '$(Build.ArtifactStagingDirectory)/HtmlReport.zip'
continueOnError: true
condition: and(succeededOrFailed(), contains(variables['runTests.TESTS_RAN'], 'True')) # if tests did not run, there is nothing to do
# Create HtmlReport artifact. This serves two purposes:
# 1. It is the way we are going to share the HtmlReport with the publish_html job that is executed on a Windows machine.
# 2. Users can download this if they want.
- task: PublishPipelineArtifact@1
displayName: 'Publish Artifact: HtmlReport'
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)/HtmlReport.zip'
artifactName: HtmlReport-sim
continueOnError: true
condition: and(succeededOrFailed(), contains(variables['runTests.TESTS_RAN'], 'True')) # if tests did not run, there is nothing to do

Просмотреть файл

@ -0,0 +1,83 @@
# This job will parse all the labels present in a PR, will set
# the tags for the build AND will set a number of configuration
# variables to be used by the rest of the projects
steps:
- checkout: self # https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=azure-devops&tabs=schema#checkout
clean: true # Executes: git clean -ffdx && git reset --hard HEAD
submodules: false
- pwsh: |
Import-Module $Env:SYSTEM_DEFAULTWORKINGDIRECTORY/tools/devops/automation/scripts/GitHub.psm1
Import-Module $Env:SYSTEM_DEFAULTWORKINGDIRECTORY/tools/devops/automation/scripts/VSTS.psm1
$buildReason = "$(Build.Reason)"
$buildSourceBranchName = "$(Build.SourceBranchName)"
$buildSourceBranch = "$(Build.SourceBranch)"
Write-Host "buildReason: ${buildReason}"
Write-Host "buildSourceBranchName: ${buildSourceBranchName}"
Write-Host "buildSourceBranch: $buildSourceBranch"
# the following list will be used to track the tags and set them in VSTS to make the monitoring person life easier
[System.Collections.Generic.List[string]]$tags = @()
if ($buildReason -eq "PullRequest" -or (($buildReason -eq "Manual") -and ($buildSourceBranchName -eq "merge")) ) {
Write-Host "Configuring build from PR."
# This is an interesting step, we do know we are dealing with a PR, but we need the PR id to
# be able to get the labels, the buildSourceBranch follows the pattern: refs/pull/{ChangeId}/merge
# we could use a regexp but then we would have two problems instead of one
$changeId = $buildSourceBranch.Replace("refs/pull/", "").Replace("/merge", "")
$prInfo = Get-GitHubPRInfo -ChangeId $changeId
Write-Host $prInfo
# make peoples life better, loop over the labels and add them as tags in the vsts build
foreach ($labelInfo in $prInfo.labels) {
$labelName = $labelInfo.name
Write-Host "Found label $labelName"
$tags.Add($labelName)
$tagsCount = $tags.Count
Write-Host "Tags count $tagsCount"
}
# special tag, we want to know if we are using a pr
$tags.Add("prBuild")
# special tag, lets add the target branch, will be useful to the users
$ref = $prInfo.base.ref
$tags.Add("$ref")
# set output variables based on the git labels
$labelsOfInterest = @(
"build-package",
"run-internal-tests",
"skip-packages",
"skip-nugets",
"skip-signing",
"skip-external-tests",
"trigger-device-tests",
"run-sample-tests",
"skip-packaged-xamarin-mac-tests",
"skip-public-jenkins",
"skip-api-comparison"
)
foreach ($l in $labelsOfInterest) {
$labelPresent = 1 -eq ($prInfo.labels | Where-Object { $_.name -eq "$l"}).Count
Write-Host "##vso[task.setvariable variable=$l;isOutput=true]$labelPresent"
}
Write-Host "##vso[task.setvariable variable=prBuild;isOutput=true]True"
} else {
# set the name of the branch under build
$tags.Add("$buildSourceBranchName")
Write-Host "##vso[task.setvariable variable=prBuild;isOutput=true]False"
}
# set the tags using the cmdlet
$tagCount = $tags.Count
Write-Host "Found '$tagsCount' tags"
Set-BuildTags -Tags $tags.ToArray()
env:
BUILD_REVISION: $(Build.SourceVersion)
GITHUB_TOKEN: $(GitHub.Token)
ACCESSTOKEN: $(AzDoBuildAccess.Token)
name: labels
displayName: 'Configure build'

Просмотреть файл

@ -0,0 +1,38 @@
steps:
- checkout: self
persistCredentials: true
# download the common artifacts + the api diff
- template: ../common/download-artifacts.yml
parameters:
devicePrefix: sim
# Download the Html Report that was added by the tests job.
- task: DownloadPipelineArtifact@2
displayName: 'Download API diff (from stable)'
inputs:
patterns: 'apidiff-stable/apidiff-stable.zip'
allowFailedBuilds: true
path: $(System.DefaultWorkingDirectory)/Reports
- task: ExtractFiles@1
displayName: 'Extract API diff (from stable)'
inputs:
archiveFilePatterns: '$(System.DefaultWorkingDirectory)/Reports/apidiff-stable/apidiff-stable.zip'
destinationFolder: '$(System.DefaultWorkingDirectory)/apidiff-stable'
- powershell: |
Get-ChildItem -Recurse $Env:SYSTEM_DEFAULTWORKINGDIRECTORY
Write-Host "##vso[task.setvariable variable=STABLE_APIDDIFF_PATH]$Env:SYSTEM_DEFAULTWORKINGDIRECTORY\apidiff-stable"
displayName: Pusblish apidiff paths
name: apidiff # not to be confused with the displayName, this is used to later use the name of the step to access the output variables from an other job
# download the artifacts.json, which will use to find the URI of the built pkg to later be given to the user
- task: DownloadPipelineArtifact@2
displayName: Download artifacts.json
inputs:
patterns: '**/*.json'
allowFailedBuilds: true
path: $(Build.SourcesDirectory)/artifacts

Просмотреть файл

@ -0,0 +1,79 @@
# Job that will download the other artifact from the tests job and will publish them in the
# vsdrops
###########################################################
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING #
###########################################################
# This job is executed on WINDOWS! make sure you DO NOT USE
# bash or linux file paths on scripts. Another important
# details is that System.DefaultWorkingDirectory
# on mac os x points on the top dir while on windows
# is the checked out dir
parameters:
- name: vsdropsPrefix
type: string
steps:
- checkout: self
persistCredentials: true
- template: download-artifacts.yml
- pwsh: |
Import-Module $Env:SYSTEM_DEFAULTWORKINGDIRECTORY\xamarin-macios\tools\devops\automation\scripts\GitHub.psm1
$apiDiffRoot = "$Env:STABLE_APIDDIFF_PATH"
$filePatterns = @{
"iOS" = "ios-*.md";
"macOS"="macos-*.md";
"tvOS"="tvos-*.md";
"watchOS"="watchos-*.md"
}
[System.Collections.Generic.List[string]]$gistsObj = @()
[System.Collections.Generic.List[string]]$gists = @{}
foreach ($key in $filePatterns.Keys) {
$filter = $filePatterns[$key]
$fileName = Get-ChildItem $apiDiffRoot -Filter $filter -Name
if ($fileName) {
$obj = New-GistObjectDefinition -Name $fileName -Path "$apiDiffRoot\$fileName" -Type "markdown"
$gistsObj.Add($obj)
# create a gist just for this file
$url = New-GistWithFiles -Description "$key API diff from stable" -Files @($obj)
Write-Host "New gist created at $url"
$gists.Add($key, $url)
}
}
# create a gist with all diffs
$url = New-GistWithFiles -Description "API diff from stable (all platforms)" -Files $gistsObj
$gists.Add("all", $url)
# set env variables to be used in later jobs
foreach ($key in $gists.Keys) {
$envVarName = "$($key.ToUpper())_STABLE_URL" # results in ALL_STABLE_URL/IOS_STABLE_URL etc
$url = $gists[$key]
Write-Host "##vso[task.setvariable variable=$envVarName]$url"
}
displayName: 'Create API from stable diff gists'
timeoutInMinutes: 1
env:
GITHUB_TOKEN: $(GitHub.Token)
- pwsh: |
Write-Host "Writing comment!"
displayName: 'Generating GitHub Comment'
condition: always()
timeoutInMinutes: 1
- pwsh: |
Get-ChildItem -Recurse $Env:SYSTEM_DEFAULTWORKINGDIRECTORY
displayName: 'Add summaries'
condition: always()
timeoutInMinutes: 1

Просмотреть файл

@ -0,0 +1,149 @@
# template that contains all the different steps to create a pkgs, publish the results and provide feedback to the
# developers in github.
parameters:
- name: runTests
type: boolean
default: true
- name: vsdropsPrefix
type: string
- name: runDeviceTests
type: boolean
default: true
jobs:
- job: configure
displayName: 'Configure build'
pool:
vmImage: ubuntu-latest
steps:
- template: configure.yml
- job: AgentPoolSelector # https://docs.microsoft.com/en-us/azure/devops/pipelines/process/phases?view=azure-devops&tabs=yaml
pool: # Consider using an agentless (server) job here, but would need to host selection logic as an Azure function: https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=azure-devops&tabs=schema#server
vmImage: ubuntu-latest
steps:
- checkout: none # https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=azure-devops&tabs=schema#checkout
# Selects appropriate agent pool based on trigger type (PR or CI)
- template: ../agent-pool-selector.yml
- job: build
dependsOn:
- AgentPoolSelector
- configure
displayName: 'Build packages'
timeoutInMinutes: 1000
variables:
AgentPoolComputed: $[ dependencies.AgentPoolSelector.outputs['setAgentPool.AgentPoolComputed'] ]
# add all the variables that have been parsed by the configuration step. Could we have a less verbose way??
#
# build-package
# run-internal-tests
# skip-packages
# skip-nugets
# skip-signing
# skip-external-tests
# trigger-device-tests
# run-sample-tests
# skip-packaged-xamarin-mac-tests
BuildPackage: $[ dependencies.configure.outputs['labels.build-package'] ]
RunInternalTests: $[ dependencies.configure.outputs['labels.run-internal-tests'] ]
SkipPackages: $[ dependencies.configure.outputs['labels.skip-packages'] ]
SkipNugets: $[ dependencies.configure.outputs['labels.skip-nugets'] ]
SkipSigning: $[ dependencies.configure.outputs['labels.skip-signing'] ]
SkipExternalTests: $[ dependencies.configure.outputs['labels.skip-external-tests'] ]
TriggerDeviceTests: $[ dependencies.configure.outputs['labels.trigger-device-tests'] ]
RunSampleTests: $[ dependencies.configure.outputs['labels.run-sample-tests'] ]
SkipPackagedXamarinMacTests: $[ dependencies.configure.outputs['labels.skip-packaged-xamarin-mac-tests'] ]
SkipPublicJenkins: $[ dependencies.configure.outputs['labels.skip-public-jenkins'] ]
SkipApiComparison: $[ dependencies.configure.outputs['labels.skip-api-comparison'] ]
# set the branch variable name, this is required by jenkins and we have a lot of scripts that depend on it
BRANCH_NAME: $(Build.SourceBranchName)
pool:
name: $(AgentPoolComputed)
demands:
- Agent.OS -equals Darwin
- Agent.OSVersion -equals 10.15
workspace:
clean: all
steps:
- template: build.yml
parameters:
runTests: ${{ parameters.runTests }}
runDeviceTests: ${{ parameters.runDeviceTests }}
vsdropsPrefix: ${{ parameters.vsdropsPrefix }}
- job: upload_azure_blob
displayName: 'Upload packages to Azure'
timeoutInMinutes: 1000
dependsOn: build # can start as soon as the packages are available
condition: and(succeeded(), contains (dependencies.build.outputs['configuration.BuildPkgs'], 'True')) # only run when we do have pkgs
variables:
Parameters.outputStorageUri: ''
pool:
vmImage: 'windows-latest'
workspace:
clean: all
steps:
- template: upload-azure.yml
- job: upload_vsdrops
displayName: 'Upload test results to VSDrops'
timeoutInMinutes: 1000
dependsOn: build # can start as soon as the tests are done
condition: and(succeededOrFailed() , contains (dependencies.build.outputs['runTests.TESTS_RAN'], 'True')) # only run when we did run the tests
pool:
vmImage: 'windows-latest'
workspace:
clean: all
steps:
- template: ../common/upload-vsdrops.yml
parameters:
devicePrefix: sim
- job: upload_vsts_tests
displayName: 'Upload xml to vsts'
timeoutInMinutes: 1000
dependsOn: build # can start as soon as the tests are done
condition: and(succeededOrFailed() , contains (dependencies.build.outputs['runTests.TESTS_RAN'], 'True')) # only run when we did run the tests
pool:
vmImage: 'windows-latest'
workspace:
clean: all
steps:
- template: ../common/upload-vsts-tests.yml
parameters:
devicePrefix: sim
- job: publish_html
displayName: 'Publish Html report in VSDrops'
timeoutInMinutes: 1000
dependsOn: # has to wait for the tests to be done AND the data to be uploaded
- build
- upload_azure_blob
- upload_vsdrops
- upload_vsts_tests
condition: succeededOrFailed()
variables:
# Define the variable FOO from the previous job
# Note the use of single quotes!
TESTS_BOT: $[ dependencies.build.outputs['runTests.TESTS_BOT'] ]
TESTS_JOBSTATUS: $[ dependencies.build.outputs['runTests.TESTS_JOBSTATUS'] ]
pool:
vmImage: 'windows-latest'
workspace:
clean: all
steps:
- template: ../common/publish-html.yml
parameters:
statusContext: "Build"
vsdropsPrefix: ${{ parameters.vsdropsPrefix }}
devicePrefix: sim

Просмотреть файл

@ -0,0 +1,183 @@
steps:
- checkout: self
persistCredentials: true
# Download the Html Report that was added by the tests job.
- task: DownloadPipelineArtifact@2
displayName: Download packages
inputs:
patterns: '**'
allowFailedBuilds: true
path: $(Build.SourcesDirectory)/artifacts
- powershell : |
$packagePrefix = "https://bosstoragemirror.blob.core.windows.net/wrench/$Env:VIRTUAL_PATH/package"
$files = Get-ChildItem -Path "$(Build.SourcesDirectory)\artifacts\package" -File -Force -Name
$manifestFile = "$(Build.SourcesDirectory)\artifacts\package\manifest"
foreach ($f in $files) {
Add-Content -Path "$manifestFile" -Value "$packagePrefix/$f"
}
Add-Content -Path "$manifestFile" -Value "$packagePrefix/$artifacts.json"
Add-Content -Path "$manifestFile" -Value "$packagePrefix/manifest"
env:
VIRTUAL_PATH: $(Build.SourceBranchName)/$(Build.SourceVersion)/$(Build.BuildId)
displayName: "Build manifest"
# Important needed for the next step
- template: generate-workspace-info.yml@templates
parameters:
GitHubToken: $(GitHub.Token)
ArtifactDirectory: $(Build.SourcesDirectory)/package-internal
- task: AzureFileCopy@3
displayName: 'Publish package to Azure'
name: upload
inputs:
SourcePath: $(Build.SourcesDirectory)/artifacts/package
azureSubscription: 'Azure Releng (7b4817ae-218f-464a-bab1-a9df2d99e1e5)'
Destination: AzureBlob
storage: bosstoragemirror
ContainerName: wrench
BlobPrefix: $(Build.SourceBranchName)/$(Build.SourceVersion)/$(Build.BuildId)/package # ideally, we would use a variable for this
outputStorageUri: Parameters.outputStorageUri
outputStorageContainerSasToken: PackageSasToken
- task: AzureFileCopy@3
displayName: 'Publish manifest to Azure'
inputs:
SourcePath: $(Build.SourcesDirectory)/artifacts/package/manifest
azureSubscription: 'Azure Releng (7b4817ae-218f-464a-bab1-a9df2d99e1e5)'
Destination: AzureBlob
storage: bosstoragemirror
ContainerName: wrench
BlobPrefix: jenkins/$(Build.SourceBranchName)/$(Build.SourceVersion)
outputStorageUri: Parameters.outputStorageUri
outputStorageContainerSasToken: PackageSasToken
- task: AzureFileCopy@3
displayName: 'Publish manifest to Azure as latest'
inputs:
SourcePath: $(Build.SourcesDirectory)/artifacts/package/manifest
azureSubscription: 'Azure Releng (7b4817ae-218f-464a-bab1-a9df2d99e1e5)'
Destination: AzureBlob
storage: bosstoragemirror
ContainerName: wrench
BlobPrefix: jenkins/$(Build.SourceBranchName)/latest
outputStorageUri: Parameters.outputStorageUri
outputStorageContainerSasToken: PackageSasToken
- task: AzureFileCopy@3
displayName: 'Publish manifest to Azure per commit'
inputs:
SourcePath: $(Build.SourcesDirectory)/artifacts/package/manifest
azureSubscription: 'Azure Releng (7b4817ae-218f-464a-bab1-a9df2d99e1e5)'
Destination: AzureBlob
storage: bosstoragemirror
ContainerName: wrench
BlobPrefix: jenkins/$(Build.SourceVersion)
outputStorageUri: Parameters.outputStorageUri
outputStorageContainerSasToken: PackageSasToken
- powershell: |
$execPath="$Env:BUILD_SOURCESDIRECTORY\Xamarin.Build.Tasks\tools\BuildTasks\build-tasks.exe"
if (-not (Test-Path $execPath -PathType Leaf)) {
Write-Host "Build task not found at $execPath!"
}
$maciosPath="$Env:BUILD_SOURCESDIRECTORY"
Write-Host "Exect path is $execPath"
Write-Host "Macios path is $maciosPath"
Write-Host "$Env:VIRTUAL_PATH"
Write-Host "Artifacts url wrench/$Env:VIRTUAL_PATH/package"
Invoke-Expression "$execPath artifacts -s `"$maciosPath`" -a bosstoragemirror -c $Env:STORAGE_PASS -u `"wrench/$Env:VIRTUAL_PATH/package`" -d `"$(Build.SourcesDirectory)\artifacts\package`" -o `"$(Build.SourcesDirectory)\artifacts\package`""
env:
VIRTUAL_PATH: $(Build.SourceBranchName)/$(Build.SourceVersion)/$(Build.BuildId)
GITHUB_AUTH_TOKEN: $(GitHub.Token)
STORAGE_PASS: $(auth-xamarin-bosstoragemirror-account-key)
displayName: 'Generate artifacts.json'
# upload the artifacts.json to the build pipeline artifacts so that it can be consumed by other jobs to
# get the required urls
- task: PublishPipelineArtifact@1
displayName: 'Publish Build Artifacts'
inputs:
targetPath: $(Build.SourcesDirectory)/artifacts/package/artifacts.json
artifactName: pkg-info
continueOnError: true
- powershell: |
Import-Module $Env:SYSTEM_DEFAULTWORKINGDIRECTORY\tools\devops\automation\scripts\GitHub.psm1
Import-Module $Env:SYSTEM_DEFAULTWORKINGDIRECTORY\tools\devops\automation\scripts\VSTS.psm1
Dir "$(Build.SourcesDirectory)\artifacts\package"
# $Env:STORAGE_URI/ ends with a /, annoying
$pkgsVirtualUrl = "$Env:STORAGE_URI" +"$(Build.SourceBranchName)/$(Build.SourceVersion)/$(Build.BuildId)/package"
Write-Host "Urls is $pkgsVirtualUrl"
$pkgsPath = "$(Build.SourcesDirectory)\artifacts\package"
$iOSPkg = Get-ChildItem -Path $pkgsPath -File -Force -Name xamarin.ios-*.pkg
Write-Host "iOS PKG is $iOSPkg"
$macPkg = Get-ChildItem -Path $pkgsPath -File -Force -Name xamarin.mac-*.pkg
Write-Host "mac PKG is $iOSPkg"
if (Test-Path "$pkgsPath\$iOSPkg" -PathType Leaf) {
Set-GitHubStatus -Status "success" -Description $iOSPkg -TargetUrl "$pkgsVirtualUrl/$iOSPkg" -Context "PKG-Xamarin.iOS"
} else {
Set-GitHubStatus -Status "error" -Description "xamarin.ios pkg not found" -Context "PKG-Xamarin.iOS"
}
if (Test-Path "$pkgsPath\notarized\xamarin.ios-*.pkg" -PathType Leaf) {
Set-GitHubStatus -Status "success" -Description "$iOSPkg (Notarized)" -TargetUrl "$pkgsVirtualUrl/notarized/$iOSPkg" -Context "PKG-Xamarin.iOS-notarized"
} else {
Set-GitHubStatus -Status "error" -Description "Notarized xamarin.ios pkg not found" -Context "PKG-Xamarin.iOS-notarized"
}
if (Test-Path "$pkgsPath\xamarin.mac-*.pkg" -PathType Leaf) {
Set-GitHubStatus -Status "success" -Description "$pkgsPath" -TargetUrl "$pkgsVirtualUrl/$macPkg" -Context "PKG-Xamarin.Mac"
} else {
Set-GitHubStatus -Status "error" -Description "xamarin.mac pkg not found." -Context "PKG-Xamarin.Mac"
}
if (Test-Path "$pkgsPath\notarized\xamarin.mac-*.pkg" -PathType Leaf) {
Set-GitHubStatus -Status "success" -Description "$pkgsPath (Notarized)" -TargetUrl "$pkgsVirtualUrl/notarized/$macPkg" -Context "PKG-Xamarin.Mac-notarized"
} else {
Set-GitHubStatus -Status "error" -Description "Notarized xamarin.mac pkg not found." -Context "PKG-Xamarin.Mac-notarized"
}
if (Test-Path "$pkgsPath\bundle.zip" -PathType Leaf) {
Set-GitHubStatus -Status "success" -Description "bundle.zip" -TargetUrl "$pkgsVirtualUrl/bundle.zip" -Context "bundle.zip"
} else {
Set-GitHubStatus -Status "error" -Description "bundle.zip not found." -Context "bundle.zip"
}
if (Test-Path "$pkgsPath\msbuild.zip" -PathType Leaf) {
Set-GitHubStatus -Status "success" -Description "msbuild.zip" -TargetUrl "$pkgsVirtualUrl/msbuild.zip" -Context "msbuild.zip"
} else {
Set-GitHubStatus -Status "error" -Description "msbuild.zip not found." -Context "msbuild.zip"
}
$nugets = $files = Get-ChildItem -Path $pkgsPath -Filter *.nupkg -File -Name
foreach ($n in $nugets) {
Set-GitHubStatus -Status "success" -Description "$n" -TargetUrl "$pkgsVirtualUrl/$n" -Context "$n"
}
$msi = $files = Get-ChildItem -Path $pkgsPath -Filter *.msi -File -Name
foreach ($n in $msi) {
Set-GitHubStatus -Status "success" -Description "$n" -TargetUrl "$pkgsVirtualUrl/$n" -Context "$n"
}
env:
BUILD_REVISION: $(Build.SourceVersion)
GITHUB_TOKEN: $(GitHub.Token)
ACCESSTOKEN: $(System.AccessToken)
STORAGE_URI: $(Parameters.outputStorageUri)
displayName: 'Set GithubStatus'

Просмотреть файл

@ -0,0 +1,16 @@
#r "_provisionator/provisionator.dll"
using System.IO;
using System.Reflection;
using System.Linq;
using static Xamarin.Provisioning.ProvisioningScript;
// Provision Xcode using the xip name declared in Make.config
Xcode ("@XCODE_XIP_NAME@").XcodeSelect (allowUntrusted: true);
// provisionator knows how to deal with this items
Item ("@MONO_PACKAGE@");
Item ("@MIN_SHARPIE_URL@");
Item ("@VS_PACKAGE@");
DotNetCoreSdk ("@DOTNET_VERSION@");

Просмотреть файл

@ -13,5 +13,6 @@ Xcode ("@XCODE_XIP_NAME@").XcodeSelect (allowUntrusted: true);
Item ("@MONO_PACKAGE@"); Item ("@MONO_PACKAGE@");
Item ("@VS_PACKAGE@"); Item ("@VS_PACKAGE@");
Item ("@XI_PACKAGE@"); Item ("@XI_PACKAGE@");
DotNetCoreSdk ("@DOTNET_VERSION@");
BrewPackages ("p7zip"); BrewPackages ("p7zip");

Просмотреть файл

@ -0,0 +1,11 @@
BrewPackages (
"cmake",
"autoconf",
"automake",
"libtool",
"p7zip",
"python",
"libmagic",
"msitools",
"wget"
);

Просмотреть файл

@ -0,0 +1,88 @@
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Xamarin.Provisioning;
using Xamarin.Provisioning.Model;
var commit = Environment.GetEnvironmentVariable ("BUILD_SOURCEVERSION");
var provision_from_commit = Environment.GetEnvironmentVariable ("PROVISION_FROM_COMMIT") ?? commit;
// Looks for a variable either in the environment, or in current repo's Make.config.
// Returns null if the variable couldn't be found.
IEnumerable<string> make_config = null;
string FindConfigurationVariable (string variable, string hash = "HEAD")
{
var value = Environment.GetEnvironmentVariable (variable);
if (!string.IsNullOrEmpty (value))
return value;
if (make_config == null) {
try {
make_config = Exec ("git", "show", $"{hash}:Make.config");
} catch {
Console.WriteLine ("Could not find a Make.config");
return null;
}
}
foreach (var line in make_config) {
if (line.StartsWith (variable + "=", StringComparison.Ordinal))
return line.Substring (variable.Length + 1);
}
return null;
}
string FindVariable (string variable)
{
var value = FindConfigurationVariable (variable, provision_from_commit);
if (!string.IsNullOrEmpty (value))
return value;
throw new Exception ($"Could not find {variable} in environment nor in the commit's ({commit}) manifest.");
}
void ExecVerbose (string filename, params string[] args)
{
Console.WriteLine ($"{filename} {string.Join (" ", args)}");
Exec (filename, args);
}
bool IsAtLeastVersion(string actualVer, string minVer)
{
if (actualVer.Equals(minVer, StringComparison.OrdinalIgnoreCase))
{
return true;
}
var actualVerChars = actualVer.ToCharArray();
var minVerChars = minVer.ToCharArray();
var length = Math.Min (minVerChars.Length, actualVerChars.Length);
var i = 0;
while (i < length)
{
if (actualVerChars[i] > minVerChars[i])
{
return true;
}
else if (minVerChars[i] > actualVerChars[i])
{
return false;
}
i++;
}
if (actualVerChars.Length == minVerChars.Length)
{
return true;
}
return actualVerChars.Length > minVerChars.Length;
}

Просмотреть файл

@ -1,63 +1,10 @@
using System.Collections.Generic; #load "provision-shared.csx"
using System.IO;
using System.Net;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Xamarin.Provisioning;
using Xamarin.Provisioning.Model;
// Provision Xcode // Provision Xcode
// //
// Overrides: // Overrides:
// * The current commit can be overridden by setting the PROVISION_FROM_COMMIT variable. // * The current commit can be overridden by setting the PROVISION_FROM_COMMIT variable.
var commit = Environment.GetEnvironmentVariable ("BUILD_SOURCEVERSION");
var provision_from_commit = Environment.GetEnvironmentVariable ("PROVISION_FROM_COMMIT") ?? commit;
// Looks for a variable either in the environment, or in current repo's Make.config.
// Returns null if the variable couldn't be found.
IEnumerable<string> make_config = null;
string FindConfigurationVariable (string variable, string hash = "HEAD")
{
var value = Environment.GetEnvironmentVariable (variable);
if (!string.IsNullOrEmpty (value))
return value;
if (make_config == null) {
try {
make_config = Exec ("git", "show", $"{hash}:Make.config");
} catch {
Console.WriteLine ("Could not find a Make.config");
return null;
}
}
foreach (var line in make_config) {
if (line.StartsWith (variable + "=", StringComparison.Ordinal))
return line.Substring (variable.Length + 1);
}
return null;
}
string FindVariable (string variable)
{
var value = FindConfigurationVariable (variable, provision_from_commit);
if (!string.IsNullOrEmpty (value))
return value;
throw new Exception ($"Could not find {variable} in environment nor in the commit's ({commit}) manifest.");
}
void ExecVerbose (string filename, params string[] args)
{
Console.WriteLine ($"{filename} {string.Join (" ", args)}");
Exec (filename, args);
}
void ListXcodes () void ListXcodes ()
{ {
Console.WriteLine ($"Xcodes:"); Console.WriteLine ($"Xcodes:");

Просмотреть файл

@ -0,0 +1 @@
python-magic==0.4.15