diff --git a/.gitignore b/.gitignore index c901f491e6..72ab2eb009 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ _build *.pdb bin obj -packages +./packages ~.pmcs* .DS_Store jenkins-results @@ -33,6 +33,7 @@ tests/bcl-test/SystemXunit.csproj global*.json .idea device-tests-provisioning.csx +build-provisioning.csx mono_crash.*.json *.binlog .vscode diff --git a/jenkins/Jenkinsfile b/jenkins/Jenkinsfile index 8662c950de..62e1697e9f 100644 --- a/jenkins/Jenkinsfile +++ b/jenkins/Jenkinsfile @@ -454,9 +454,12 @@ timestamps { currentStage = "${STAGE_NAME}" echo ("Building on ${env.NODE_NAME}") sh ("env | sort") // Print out environment for debug purposes + + branchName = env.BRANCH_NAME + echo ("Branch name: ${branchName}") + scmVars = checkout scm isPr = (env.CHANGE_ID && !env.CHANGE_ID.empty ? true : false) - branchName = env.BRANCH_NAME if (isPr) { gitHash = sh (script: "git log -1 --pretty=%H refs/remotes/origin/${env.BRANCH_NAME}", returnStdout: true).trim () } else { diff --git a/jenkins/build-nugets.sh b/jenkins/build-nugets.sh deleted file mode 100755 index 1a41f70c38..0000000000 --- a/jenkins/build-nugets.sh +++ /dev/null @@ -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/ diff --git a/jenkins/build-package.sh b/jenkins/build-package.sh deleted file mode 100755 index 150b8686a7..0000000000 --- a/jenkins/build-package.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -ex - -cd "$(dirname "${BASH_SOURCE[0]}")/.." -#WORKSPACE=$(pwd) - -rm -Rf ../package -make package diff --git a/tools/devops/Makefile b/tools/devops/Makefile index 3401e86c41..3e6babfb7c 100644 --- a/tools/devops/Makefile +++ b/tools/devops/Makefile @@ -7,4 +7,22 @@ device-tests-provisioning.csx: device-tests-provisioning.csx.in Makefile $(TOP)/ -e 's#@XI_PACKAGE@#$(XI_PACKAGE)#g' \ -e 's#@MONO_PACKAGE@#$(MIN_MONO_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 diff --git a/tools/devops/automation/VSMacVersion.ps1 b/tools/devops/automation/VSMacVersion.ps1 new file mode 100644 index 0000000000..9848b419b9 --- /dev/null +++ b/tools/devops/automation/VSMacVersion.ps1 @@ -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 diff --git a/tools/devops/automation/build-pipeline.yml b/tools/devops/automation/build-pipeline.yml new file mode 100644 index 0000000000..c9ac816c02 --- /dev/null +++ b/tools/devops/automation/build-pipeline.yml @@ -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' diff --git a/tools/devops/automation/scripts/GitHub.Tests.ps1 b/tools/devops/automation/scripts/GitHub.Tests.ps1 new file mode 100644 index 0000000000..ee8cb4abc2 --- /dev/null +++ b/tools/devops/automation/scripts/GitHub.Tests.ps1 @@ -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 + } + } +} diff --git a/tools/devops/automation/scripts/GitHub.psm1 b/tools/devops/automation/scripts/GitHub.psm1 new file mode 100644 index 0000000000..3c6101ff05 --- /dev/null +++ b/tools/devops/automation/scripts/GitHub.psm1 @@ -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 diff --git a/tools/devops/automation/scripts/MLaunch.Tests.ps1 b/tools/devops/automation/scripts/MLaunch.Tests.ps1 new file mode 100644 index 0000000000..5d33639385 --- /dev/null +++ b/tools/devops/automation/scripts/MLaunch.Tests.ps1 @@ -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 + } + } +} diff --git a/tools/devops/automation/scripts/MLaunch.psm1 b/tools/devops/automation/scripts/MLaunch.psm1 new file mode 100644 index 0000000000..9e78541edc --- /dev/null +++ b/tools/devops/automation/scripts/MLaunch.psm1 @@ -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 diff --git a/tools/devops/automation/scripts/Makefile b/tools/devops/automation/scripts/Makefile new file mode 100644 index 0000000000..1e805d8ce7 --- /dev/null +++ b/tools/devops/automation/scripts/Makefile @@ -0,0 +1,5 @@ +TOP=../../../.. +include $(TOP)/Make.config + +run-tests: + $(Q_GEN) pwsh -Command "Install-Module -AcceptLicense -Force -AllowClobber Pester;Invoke-Pester" diff --git a/tools/devops/automation/scripts/System.Tests.ps1 b/tools/devops/automation/scripts/System.Tests.ps1 new file mode 100644 index 0000000000..756ed76cf3 --- /dev/null +++ b/tools/devops/automation/scripts/System.Tests.ps1 @@ -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 + } + } +} diff --git a/tools/devops/automation/scripts/System.psm1 b/tools/devops/automation/scripts/System.psm1 new file mode 100644 index 0000000000..2966bc605d --- /dev/null +++ b/tools/devops/automation/scripts/System.psm1 @@ -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 diff --git a/tools/devops/automation/scripts/VSTS.Tests.ps1 b/tools/devops/automation/scripts/VSTS.Tests.ps1 new file mode 100644 index 0000000000..367a002f92 --- /dev/null +++ b/tools/devops/automation/scripts/VSTS.Tests.ps1 @@ -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 + } + } +} diff --git a/tools/devops/automation/scripts/VSTS.psm1 b/tools/devops/automation/scripts/VSTS.psm1 new file mode 100644 index 0000000000..9983e422a2 --- /dev/null +++ b/tools/devops/automation/scripts/VSTS.psm1 @@ -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 diff --git a/tools/devops/automation/scripts/bash/build-nugets.sh b/tools/devops/automation/scripts/bash/build-nugets.sh new file mode 100755 index 0000000000..7b9db98802 --- /dev/null +++ b/tools/devops/automation/scripts/bash/build-nugets.sh @@ -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/ diff --git a/jenkins/clean-jenkins-bots.sh b/tools/devops/automation/scripts/bash/clean-bot.sh similarity index 100% rename from jenkins/clean-jenkins-bots.sh rename to tools/devops/automation/scripts/bash/clean-bot.sh diff --git a/jenkins/productsign.sh b/tools/devops/automation/scripts/bash/productsign.sh similarity index 100% rename from jenkins/productsign.sh rename to tools/devops/automation/scripts/bash/productsign.sh diff --git a/tools/devops/automation/templates/agent-pool-selector.yml b/tools/devops/automation/templates/agent-pool-selector.yml new file mode 100644 index 0000000000..1c6d7e5ab2 --- /dev/null +++ b/tools/devops/automation/templates/agent-pool-selector.yml @@ -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 }} diff --git a/tools/devops/automation/templates/common/download-artifacts.yml b/tools/devops/automation/templates/common/download-artifacts.yml new file mode 100644 index 0000000000..0f8905cd1c --- /dev/null +++ b/tools/devops/automation/templates/common/download-artifacts.yml @@ -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 diff --git a/tools/devops/automation/templates/common/publish-html.yml b/tools/devops/automation/templates/common/publish-html.yml new file mode 100644 index 0000000000..f71a167b17 --- /dev/null +++ b/tools/devops/automation/templates/common/publish-html.yml @@ -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 diff --git a/tools/devops/automation/templates/common/upload-vsdrops.yml b/tools/devops/automation/templates/common/upload-vsdrops.yml new file mode 100644 index 0000000000..223f9ce02e --- /dev/null +++ b/tools/devops/automation/templates/common/upload-vsdrops.yml @@ -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 diff --git a/tools/devops/automation/templates/common/upload-vsts-tests.yml b/tools/devops/automation/templates/common/upload-vsts-tests.yml new file mode 100644 index 0000000000..5f4e662d67 --- /dev/null +++ b/tools/devops/automation/templates/common/upload-vsts-tests.yml @@ -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() diff --git a/tools/devops/automation/templates/devices/build.yml b/tools/devops/automation/templates/devices/build.yml new file mode 100644 index 0000000000..085522a6d7 --- /dev/null +++ b/tools/devops/automation/templates/devices/build.yml @@ -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 diff --git a/tools/devops/automation/templates/devices/stage.yml b/tools/devops/automation/templates/devices/stage.yml new file mode 100644 index 0000000000..04708c1f1f --- /dev/null +++ b/tools/devops/automation/templates/devices/stage.yml @@ -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 }} diff --git a/tools/devops/automation/templates/governance-checks.yml b/tools/devops/automation/templates/governance-checks.yml new file mode 100644 index 0000000000..b432b5d52c --- /dev/null +++ b/tools/devops/automation/templates/governance-checks.yml @@ -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() diff --git a/tools/devops/automation/templates/mac-tests.yml b/tools/devops/automation/templates/mac-tests.yml new file mode 100644 index 0000000000..39f6f0fa18 --- /dev/null +++ b/tools/devops/automation/templates/mac-tests.yml @@ -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 diff --git a/tools/devops/automation/templates/packages/build.yml b/tools/devops/automation/templates/packages/build.yml new file mode 100644 index 0000000000..21a50e02b4 --- /dev/null +++ b/tools/devops/automation/templates/packages/build.yml @@ -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 diff --git a/tools/devops/automation/templates/packages/configure.yml b/tools/devops/automation/templates/packages/configure.yml new file mode 100644 index 0000000000..94d7814a0f --- /dev/null +++ b/tools/devops/automation/templates/packages/configure.yml @@ -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' diff --git a/tools/devops/automation/templates/packages/download-artifacts.yml b/tools/devops/automation/templates/packages/download-artifacts.yml new file mode 100644 index 0000000000..597c9c44a8 --- /dev/null +++ b/tools/devops/automation/templates/packages/download-artifacts.yml @@ -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 diff --git a/tools/devops/automation/templates/packages/publish-html.yml b/tools/devops/automation/templates/packages/publish-html.yml new file mode 100644 index 0000000000..24e86d51cb --- /dev/null +++ b/tools/devops/automation/templates/packages/publish-html.yml @@ -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 diff --git a/tools/devops/automation/templates/packages/stage.yml b/tools/devops/automation/templates/packages/stage.yml new file mode 100644 index 0000000000..a00e8d146b --- /dev/null +++ b/tools/devops/automation/templates/packages/stage.yml @@ -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 diff --git a/tools/devops/automation/templates/packages/upload-azure.yml b/tools/devops/automation/templates/packages/upload-azure.yml new file mode 100644 index 0000000000..bf3534e5e6 --- /dev/null +++ b/tools/devops/automation/templates/packages/upload-azure.yml @@ -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' diff --git a/tools/devops/build-provisioning.csx.in b/tools/devops/build-provisioning.csx.in new file mode 100644 index 0000000000..767a435a88 --- /dev/null +++ b/tools/devops/build-provisioning.csx.in @@ -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@"); diff --git a/tools/devops/device-tests-provisioning.csx.in b/tools/devops/device-tests-provisioning.csx.in index 20f1a03676..cac7d85844 100644 --- a/tools/devops/device-tests-provisioning.csx.in +++ b/tools/devops/device-tests-provisioning.csx.in @@ -13,5 +13,6 @@ Xcode ("@XCODE_XIP_NAME@").XcodeSelect (allowUntrusted: true); Item ("@MONO_PACKAGE@"); Item ("@VS_PACKAGE@"); Item ("@XI_PACKAGE@"); +DotNetCoreSdk ("@DOTNET_VERSION@"); BrewPackages ("p7zip"); diff --git a/tools/devops/provision-brew-packages.csx b/tools/devops/provision-brew-packages.csx new file mode 100644 index 0000000000..728b20602a --- /dev/null +++ b/tools/devops/provision-brew-packages.csx @@ -0,0 +1,11 @@ +BrewPackages ( + "cmake", + "autoconf", + "automake", + "libtool", + "p7zip", + "python", + "libmagic", + "msitools", + "wget" + ); diff --git a/tools/devops/provision-shared.csx b/tools/devops/provision-shared.csx new file mode 100644 index 0000000000..8846c8be77 --- /dev/null +++ b/tools/devops/provision-shared.csx @@ -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 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; +} diff --git a/tools/devops/provision-xcode.csx b/tools/devops/provision-xcode.csx index 589e16857e..3a9c2e2586 100644 --- a/tools/devops/provision-xcode.csx +++ b/tools/devops/provision-xcode.csx @@ -1,63 +1,10 @@ -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; +#load "provision-shared.csx" // Provision Xcode // // Overrides: // * 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 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 () { Console.WriteLine ($"Xcodes:"); diff --git a/tools/devops/python-dependencies.txt b/tools/devops/python-dependencies.txt new file mode 100644 index 0000000000..6863aff602 --- /dev/null +++ b/tools/devops/python-dependencies.txt @@ -0,0 +1 @@ +python-magic==0.4.15