commit d36fec9074a321688fe8d37ae455c6f1490eaabe Author: Maxim Lobanov Date: Thu Apr 16 16:58:11 2020 +0300 Move helpers to the separate repository diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..517657b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to make participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all project spaces, and it also applies when +an individual is representing the project or its community in public spaces. +Examples of representing a project or community include using an official +project e-mail address, posting via an official social media account, or acting +as an appointed representative at an online or offline event. Representation of +a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at opensource@github.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fbc5fc5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +## Contributing + +[fork]: https://github.com/actions/versions-package-tools/fork +[pr]: https://github.com/actions/versions-package-tools/compare +[code-of-conduct]: CODE_OF_CONDUCT.md + +Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. + +Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [MIT](LICENSE.md). + +Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms. + +## Submitting a pull request + +1. [Fork][fork] and clone the repository +1. Create a new branch: `git checkout -b my-branch-name` +1. Make your changes +1. Push to your fork and [submit a pull request][pr] +1. Make sure that checks in your pull request are green + +Here are a few things you can do that will increase the likelihood of your pull request being accepted: + +- Please include a summary of the change and which issue is fixed. Also include relevant motivation and context. +- Follow the style guide for [PowerShell](https://github.com/PoshCode/PowerShellPracticeAndStyle). +- Write [good commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). + +## Resources + +- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) +- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) +- [GitHub Help](https://help.github.com) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f42f5ab --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 GitHub + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d9d6ccf --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Common tools for generation of packages in the actions/*-versions repositories +This repository contains PowerShell modules that are used to generate packages for Actions. The packages are consumed by the images generated through [actions/virtual-environments](https://github.com/actions/virtual-environments) and some of the setup-* Actions + +## Contribution +Contributions are welcome! See [Contributor's Guide](./CONTRIBUTING.md) for more details about contribution process and code structure diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..f0b196f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,3 @@ +If you discover a security issue in this repo, please submit it through the [GitHub Security Bug Bounty](https://hackerone.com/github) + +Thanks for helping make GitHub Actions safe for everyone. diff --git a/azure-devops/azure-devops-api.ps1 b/azure-devops/azure-devops-api.ps1 new file mode 100644 index 0000000..dd2feba --- /dev/null +++ b/azure-devops/azure-devops-api.ps1 @@ -0,0 +1,89 @@ +class AzureDevOpsApi +{ + [string] $BaseUrl + [string] $RepoOwner + [object] $AuthHeader + + AzureDevOpsApi( + [string] $TeamFoundationCollectionUri, + [string] $ProjectName, + [string] $AccessToken + ) { + $this.BaseUrl = $this.BuildBaseUrl($TeamFoundationCollectionUri, $ProjectName) + $this.AuthHeader = $this.BuildAuth($AccessToken) + } + + [object] hidden BuildAuth([string]$AccessToken) { + if ([string]::IsNullOrEmpty($AccessToken)) { + return $null + } + return @{ + Authorization = "Bearer $AccessToken" + } + } + + [string] hidden BuildBaseUrl([string]$TeamFoundationCollectionUri, [string]$ProjectName) { + return "${TeamFoundationCollectionUri}/${ProjectName}/_apis" + } + + [object] QueueBuild([string]$ToolVersion, [string]$SourceBranch, [string]$SourceVersion, [UInt32]$DefinitionId){ + $url = "build/builds" + + # The content of parameters field should be a json string + $buildParameters = @{ VERSION = $ToolVersion } | ConvertTo-Json + + $body = @{ + definition = @{ + id = $DefinitionId + } + sourceBranch = $SourceBranch + sourceVersion = $SourceVersion + parameters = $buildParameters + } | ConvertTo-Json + + return $this.InvokeRestMethod($url, 'POST', $body) + } + + [object] GetBuildInfo([UInt32]$BuildId){ + $url = "build/builds/$BuildId" + + return $this.InvokeRestMethod($url, 'GET', $null) + } + + [string] hidden BuildUrl([string]$Url) { + return "$($this.BaseUrl)/${Url}/?api-version=5.1" + } + + [object] hidden InvokeRestMethod( + [string] $Url, + [string] $Method, + [string] $Body + ) { + $requestUrl = $this.BuildUrl($Url) + $params = @{ + Method = $Method + ContentType = "application/json" + Uri = $requestUrl + Headers = @{} + } + if ($this.AuthHeader) { + $params.Headers += $this.AuthHeader + } + if (![string]::IsNullOrEmpty($body)) { + $params.Body = $Body + } + + return Invoke-RestMethod @params + } + +} + +function Get-AzureDevOpsApi { + param ( + [string] $TeamFoundationCollectionUri, + [string] $ProjectName, + [string] $AccessToken + ) + + return [AzureDevOpsApi]::New($TeamFoundationCollectionUri, $ProjectName, $AccessToken) +} \ No newline at end of file diff --git a/azure-devops/build-info.ps1 b/azure-devops/build-info.ps1 new file mode 100644 index 0000000..9622d8c --- /dev/null +++ b/azure-devops/build-info.ps1 @@ -0,0 +1,44 @@ +Import-Module (Join-Path $PSScriptRoot "azure-devops-api.ps1") + +class BuildInfo +{ + [AzureDevOpsApi] $AzureDevOpsApi + [String] $Name + [UInt32] $Id + [String] $Status + [String] $Result + [String] $Link + + BuildInfo([AzureDevOpsApi] $AzureDevOpsApi, [object] $Build) + { + $this.AzureDevOpsApi = $AzureDevOpsApi + $this.Id = $Build.id + $this.Name = $Build.buildNumber + $this.Link = $Build._links.web.href + $this.Status = $Build.status + $this.Result = $Build.result + } + + [boolean] IsFinished() { + return ($this.Status -eq "completed") -or ($this.Status -eq "cancelling") + } + + [boolean] IsSuccess() { + return $this.Result -eq "succeeded" + } + + [void] UpdateBuildInfo() { + $buildInfo = $this.AzureDevOpsApi.GetBuildInfo($this.Id) + $this.Status = $buildInfo.status + $this.Result = $buildInfo.result + } +} + +function Get-BuildInfo { + param ( + [AzureDevOpsApi] $AzureDevOpsApi, + [object] $Build + ) + + return [BuildInfo]::New($AzureDevOpsApi, $Build) +} \ No newline at end of file diff --git a/azure-devops/run-ci-builds.ps1 b/azure-devops/run-ci-builds.ps1 new file mode 100644 index 0000000..bf11295 --- /dev/null +++ b/azure-devops/run-ci-builds.ps1 @@ -0,0 +1,94 @@ +param ( + [Parameter(Mandatory)] [string] $TeamFoundationCollectionUri, + [Parameter(Mandatory)] [string] $AzureDevOpsProjectName, + [Parameter(Mandatory)] [string] $AzureDevOpsAccessToken, + [Parameter(Mandatory)] [string] $SourceBranch, + [Parameter(Mandatory)] [string] $ToolVersions, + [Parameter(Mandatory)] [UInt32] $DefinitionId, + [string] $SourceVersion +) + +Import-Module (Join-Path $PSScriptRoot "azure-devops-api.ps1") +Import-Module (Join-Path $PSScriptRoot "build-info.ps1") + +function Queue-Builds { + param ( + [Parameter(Mandatory)] [AzureDevOpsApi] $AzureDevOpsApi, + [Parameter(Mandatory)] [string] $ToolVersions, + [Parameter(Mandatory)] [string] $SourceBranch, + [Parameter(Mandatory)] [string] $SourceVersion, + [Parameter(Mandatory)] [string] $DefinitionId + ) + + [BuildInfo[]]$queuedBuilds = @() + + $ToolVersions.Split(',') | ForEach-Object { + $version = $_.Trim() + Write-Host "Queue build for $version..." + $queuedBuild = $AzureDevOpsApi.QueueBuild($version, $SourceBranch, $SourceVersion, $DefinitionId) + $buildInfo = Get-BuildInfo -AzureDevOpsApi $AzureDevOpsApi -Build $queuedBuild + Write-Host "Queued build: $($buildInfo.Link)" + $queuedBuilds += $buildInfo + } + + return $queuedBuilds +} + +function Wait-Builds { + param ( + [Parameter(Mandatory)] [BuildInfo[]] $Builds + ) + + $timeoutBetweenRefreshSec = 30 + + do { + # If build is still running - refresh its status + foreach($build in $builds) { + if (!$build.IsFinished()) { + $build.UpdateBuildInfo() + + if ($build.IsFinished()) { + Write-Host "The $($build.Name) build was completed: $($build.Link)" + } + } + } + + $runningBuildsCount = ($builds | Where-Object { !$_.IsFinished() }).Length + + Start-Sleep -Seconds $timeoutBetweenRefreshSec + } while($runningBuildsCount -gt 0) +} + +function Make-BuildsOutput { + param ( + [Parameter(Mandatory)] [BuildInfo[]] $Builds + ) + + Write-Host "Builds info:" + $builds | Format-Table -AutoSize -Property Name,Id,Status,Result,Link | Out-String -Width 10000 + + # Return exit code based on status of builds + $failedBuilds = ($builds | Where-Object { !$_.IsSuccess() }) + if ($failedBuilds.Length -ne 0) { + Write-Host "##vso[task.logissue type=error;]Builds failed" + $failedBuilds | ForEach-Object -Process { Write-Host "##vso[task.logissue type=error;]Name: $($_.Name); Link: $($_.Link)" } + Write-Host "##vso[task.complete result=Failed]" + } else { + Write-host "##[section] All builds have been passed successfully" + } +} + +$azureDevOpsApi = Get-AzureDevOpsApi -TeamFoundationCollectionUri $TeamFoundationCollectionUri ` + -ProjectName $AzureDevOpsProjectName ` + -AccessToken $AzureDevOpsAccessToken + +$queuedBuilds = Queue-Builds -AzureDevOpsApi $azureDevOpsApi ` + -ToolVersions $ToolVersions ` + -SourceBranch $SourceBranch ` + -SourceVersion $SourceVersion ` + -DefinitionId $DefinitionId + +Write-Host "Waiting results of builds ..." +Wait-Builds -Builds $queuedBuilds + +Make-BuildsOutput -Builds $queuedBuilds diff --git a/github/create-pull-request.ps1 b/github/create-pull-request.ps1 new file mode 100644 index 0000000..9c01042 --- /dev/null +++ b/github/create-pull-request.ps1 @@ -0,0 +1,106 @@ +<# +.SYNOPSIS +Create commit with all unstaged changes in repository and create pull-request + +.PARAMETER RepositoryOwner +Required parameter. The organization which tool repository belongs +.PARAMETER RepositoryName +Optional parameter. The name of tool repository +.PARAMETER AccessToken +Required parameter. PAT Token to authorize +.PARAMETER BranchName +Required parameter. The name of branch where changes will be pushed +.PARAMETER CommitMessage +Required parameter. The commit message to push changes +.PARAMETER PullRequestTitle +Required parameter. The title of pull-request +.PARAMETER PullRequestBody +Required parameter. The description of pull-request +#> +param ( + [Parameter(Mandatory)] [string] $RepositoryOwner, + [Parameter(Mandatory)] [string] $RepositoryName, + [Parameter(Mandatory)] [string] $AccessToken, + [Parameter(Mandatory)] [string] $BranchName, + [Parameter(Mandatory)] [string] $CommitMessage, + [Parameter(Mandatory)] [string] $PullRequestTitle, + [Parameter(Mandatory)] [string] $PullRequestBody +) + +Import-Module (Join-Path $PSScriptRoot "github-api.psm1") +Import-Module (Join-Path $PSScriptRoot "git.psm1") + +function Update-PullRequest { + Param ( + [Parameter(Mandatory=$true)] + [object] $GitHubApi, + [Parameter(Mandatory=$true)] + [string] $Title, + [Parameter(Mandatory=$true)] + [string] $Body, + [Parameter(Mandatory=$true)] + [string] $BranchName, + [Parameter(Mandatory=$true)] + [object] $PullRequest + ) + + $updatedPullRequest = $GitHubApi.UpdatePullRequest($Title, $Body, $BranchName, $PullRequest.number) + + if (($updatedPullRequest -eq $null) -or ($updatedPullRequest.html_url -eq $null)) { + Write-Host "##vso[task.logissue type=error;] Unexpected error occurs while updating pull request." + exit 1 + } + Write-host "##[section] Pull request updated: $($updatedPullRequest.html_url)" +} + +function Create-PullRequest { + Param ( + [Parameter(Mandatory=$true)] + [object] $GitHubApi, + [Parameter(Mandatory=$true)] + [string] $Title, + [Parameter(Mandatory=$true)] + [string] $Body, + [Parameter(Mandatory=$true)] + [string] $BranchName + ) + + $createdPullRequest = $GitHubApi.CreateNewPullRequest($Title, $Body, $BranchName) + + if (($createdPullRequest -eq $null) -or ($createdPullRequest.html_url -eq $null)) { + Write-Host "##vso[task.logissue type=error;] Unexpected error occurs while creating pull request." + exit 1 + } + + Write-host "##[section] Pull request created: $($createdPullRequest.html_url)" +} + +Write-Host "Configure local git preferences" +Git-ConfigureUser -Name "Service account" -Email "no-reply@microsoft.com" + +Write-Host "Create branch: $BranchName" +Git-CreateBranch -Name $BranchName + +Write-Host "Create commit" +Git-CommitAllChanges -Message $CommitMessage + +Write-Host "Push branch: $BranchName" +Git-PushBranch -Name $BranchName -Force $true + +$gitHubApi = Get-GitHubApi -AccountName $RepositoryOwner -ProjectName $RepositoryName -AccessToken $AccessToken +$pullRequest = $gitHubApi.GetPullRequest($BranchName, $RepositoryOwner) + +if ($pullRequest.Count -gt 0) { + Write-Host "Update pull request" + Update-PullRequest -GitHubApi $gitHubApi ` + -Title $PullRequestTitle ` + -Body $PullRequestBody ` + -BranchName $BranchName ` + -PullRequest $pullRequest[0] +} else { + Write-Host "Create pull request" + Create-PullRequest -GitHubApi $gitHubApi ` + -Title $PullRequestTitle ` + -Body $PullRequestBody ` + -BranchName $BranchName +} diff --git a/github/git.psm1 b/github/git.psm1 new file mode 100644 index 0000000..2383a79 --- /dev/null +++ b/github/git.psm1 @@ -0,0 +1,81 @@ +<# +.SYNOPSIS +Configure git credentials to use with commits +#> +function Git-ConfigureUser { + Param ( + [Parameter(Mandatory=$true)] + [string] $Name, + [Parameter(Mandatory=$true)] + [string] $Email + ) + + git config --global user.name $Name | Out-Host + git config --global user.email $Email | Out-Host + + if ($LASTEXITCODE -ne 0) { + Write-Host "##vso[task.logissue type=error;] Unexpected failure occurs while configuring git preferences." + exit 1 + } +} + +<# +.SYNOPSIS +Create new branch +#> +function Git-CreateBranch { + Param ( + [Parameter(Mandatory=$true)] + [string] $Name + ) + + git checkout -b $Name | Out-Host + + if ($LASTEXITCODE -ne 0) { + Write-Host "##vso[task.logissue type=error;] Unexpected failure occurs while creating new branch: $Name." + exit 1 + } +} + +<# +.SYNOPSIS +Commit all staged and unstaged changes +#> +function Git-CommitAllChanges { + Param ( + [Parameter(Mandatory=$true)] + [string] $Message + ) + + git add -A | Out-Host + git commit -m "$Message" | Out-Host + + if ($LASTEXITCODE -ne 0) { + Write-Host "##vso[task.logissue type=error;] Unexpected failure occurs while commiting changes." + exit 1 + } +} + +<# +.SYNOPSIS +Push branch to remote repository +#> +function Git-PushBranch { + Param ( + [Parameter(Mandatory=$true)] + [string] $Name, + [Parameter(Mandatory=$true)] + [boolean] $Force + ) + + if ($Force) { + git push --set-upstream origin $Name --force | Out-Host + } else { + git push --set-upstream origin $Name | Out-Host + } + + if ($LASTEXITCODE -ne 0) { + Write-Host "##vso[task.logissue type=error;] Unexpected failure occurs while pushing changes." + exit 1 + } +} \ No newline at end of file diff --git a/github/github-api.psm1 b/github/github-api.psm1 new file mode 100644 index 0000000..fef64a5 --- /dev/null +++ b/github/github-api.psm1 @@ -0,0 +1,109 @@ +<# +.SYNOPSIS +The module that contains a bunch of methods to interact with GitHub API V3 +#> +class GitHubApi +{ + [string] $BaseUrl + [string] $RepoOwner + [object] $AuthHeader + + GitHubApi( + [string] $AccountName, + [string] $ProjectName, + [string] $AccessToken + ) { + $this.BaseUrl = $this.BuildBaseUrl($AccountName, $ProjectName) + $this.AuthHeader = $this.BuildAuth($AccessToken) + } + + [object] hidden BuildAuth([string]$AccessToken) { + if ([string]::IsNullOrEmpty($AccessToken)) { + return $null + } + $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("'':${AccessToken}")) + return @{ + Authorization = "Basic ${base64AuthInfo}" + } + } + + [string] hidden BuildBaseUrl([string]$RepositoryOwner, [string]$RepositoryName) { + return "https://api.github.com/repos/$RepositoryOwner/$RepositoryName" + } + + [object] CreateNewPullRequest([string]$Title, [string]$Body, [string]$BranchName){ + $requestBody = @{ + title = $Title + body = $Body + head = $BranchName + base = "master" + } | ConvertTo-Json + + $url = "pulls" + return $this.InvokeRestMethod($url, 'Post', $null, $requestBody) + } + + [object] GetPullRequest([string]$BranchName, [string]$RepositoryOwner){ + $url = "pulls" + return $this.InvokeRestMethod($url, 'GET', "head=${RepositoryOwner}:$BranchName&base=master", $null) + } + + [object] UpdatePullRequest([string]$Title, [string]$Body, [string]$BranchName, [string]$PullRequestNumber){ + $requestBody = @{ + title = $Title + body = $Body + head = $BranchName + base = "master" + } | ConvertTo-Json + + $url = "pulls/$PullRequestNumber" + return $this.InvokeRestMethod($url, 'Post', $null, $requestBody) + } + + [object] GetGitHubReleases(){ + $url = "releases" + return $this.InvokeRestMethod($url, 'GET', $null, $null) + } + + [string] hidden BuildUrl([string]$Url, [string]$RequestParams) { + if ([string]::IsNullOrEmpty($RequestParams)) { + return "$($this.BaseUrl)/$($Url)" + } else { + return "$($this.BaseUrl)/$($Url)?$($RequestParams)" + } + } + + [object] hidden InvokeRestMethod( + [string] $Url, + [string] $Method, + [string] $RequestParams, + [string] $Body + ) { + $requestUrl = $this.BuildUrl($Url, $RequestParams) + $params = @{ + Method = $Method + ContentType = "application/json" + Uri = $requestUrl + Headers = @{} + } + if ($this.AuthHeader) { + $params.Headers += $this.AuthHeader + } + if (![string]::IsNullOrEmpty($Body)) { + $params.Body = $Body + } + + return Invoke-RestMethod @params + } + +} + +function Get-GitHubApi { + param ( + [string] $AccountName, + [string] $ProjectName, + [string] $AccessToken + ) + + return [GitHubApi]::New($AccountName, $ProjectName, $AccessToken) +} \ No newline at end of file diff --git a/packages-generation/common-helpers.psm1 b/packages-generation/common-helpers.psm1 new file mode 100644 index 0000000..8de58a7 --- /dev/null +++ b/packages-generation/common-helpers.psm1 @@ -0,0 +1,80 @@ +<# +.SYNOPSIS +The execute command and print all output to the logs +#> +function Execute-Command { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)][string] $Command + ) + + Write-Debug "Execute $Command" + + try { + Invoke-Expression $Command | ForEach-Object { Write-Host $_ } + } + catch { + Write-Host "Error happened during command execution: $Command" + Write-Host "##vso[task.logissue type=error;] $_" + } +} + +<# +.SYNOPSIS +Download file from url and return local path to file +#> +function Download-File { + param( + [Parameter(Mandatory=$true)] + [Uri]$Uri, + [Parameter(Mandatory=$true)] + [String]$OutputFolder + ) + + $targetFilename = [IO.Path]::GetFileName($Uri) + $targetFilepath = Join-Path $OutputFolder $targetFilename + + Write-Debug "Download source from $Uri to $OutFile" + try { + (New-Object System.Net.WebClient).DownloadFile($Uri, $targetFilepath) + return $targetFilepath + } catch { + Write-Host "Error during downloading file from '$Uri'" + "$_" + exit 1 + } +} + +<# +.SYNOPSIS +Generate file that contains the list of all files in particular directory +#> +function New-ToolStructureDump { + param( + [Parameter(Mandatory=$true)] + [String]$ToolPath, + [Parameter(Mandatory=$true)] + [String]$OutputFolder + ) + + $outputFile = Join-Path $OutputFolder "tools_structure.txt" + + $folderContent = Get-ChildItem -Path $ToolPath -Recurse | Sort-Object | Select-Object -Property FullName, Length + $folderContent | ForEach-Object { + $relativePath = $_.FullName.Replace($ToolPath, ""); + return "${relativePath}" + } | Out-File -FilePath $outputFile +} + +<# +.SYNOPSIS +Check if it is macOS / Ubuntu platform +#> +function IsNixPlatform { + param( + [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] + [String]$Platform + ) + + return ($Platform -match "macos") -or ($Platform -match "ubuntu") +} \ No newline at end of file diff --git a/packages-generation/generate-versions-manifest.ps1 b/packages-generation/generate-versions-manifest.ps1 new file mode 100644 index 0000000..532e598 --- /dev/null +++ b/packages-generation/generate-versions-manifest.ps1 @@ -0,0 +1,145 @@ +<# +.SYNOPSIS +Generate versions manifest based on repository releases + +.DESCRIPTION +Versions manifest is needed to find the latest assets for particular version of tool +.PARAMETER GitHubRepositoryOwner +Required parameter. The organization which tool repository belongs +.PARAMETER GitHubRepositoryName +Optional parameter. The name of tool repository +.PARAMETER GitHubAccessToken +Required parameter. PAT Token to overcome GitHub API Rate limit +.PARAMETER OutputFile +Required parameter. File "*.json" where generated results will be saved +.PARAMETER PlatformMapFile +Optional parameter. Path to the json file with platform map +Structure example: +{ + "macos-1014": [ + { + "platform": "darwin", + "platform_version": "10.14" + }, ... + ], ... +} +#> + +param ( + [Parameter(Mandatory)] [string] $GitHubRepositoryOwner, + [Parameter(Mandatory)] [string] $GitHubRepositoryName, + [Parameter(Mandatory)] [string] $GitHubAccessToken, + [Parameter(Mandatory)] [string] $OutputFile, + [string] $PlatformMapFile +) + +Import-Module (Join-Path $PSScriptRoot "../github/github-api.psm1") + +if ($PlatformMapFile -and (Test-Path $PlatformMapFile)) { + $PlatformMap = Get-Content $PlatformMapFile -Raw | ConvertFrom-Json -AsHashtable +} else { + $PlatformMap = @{} +} + +function New-AssetItem { + param ( + [Parameter(Mandatory)][string]$Filename, + [Parameter(Mandatory)][string]$DownloadUrl, + [Parameter(Mandatory)][string]$Arch, + [Parameter(Mandatory)][string]$Platform, + [string]$PlatformVersion + ) + $asset = New-Object PSObject + + $asset | Add-Member -Name "filename" -Value $Filename -MemberType NoteProperty + $asset | Add-Member -Name "arch" -Value $Arch -MemberType NoteProperty + $asset | Add-Member -Name "platform" -Value $Platform -MemberType NoteProperty + if ($PlatformVersion) { $asset | Add-Member -Name "platform_version" -Value $PlatformVersion -MemberType NoteProperty } + $asset | Add-Member -Name "download_url" -Value $DownloadUrl -MemberType NoteProperty + + return $asset +} + +function Build-AssetsList { + param ( + [AllowEmptyCollection()] + [Parameter(Mandatory)][array]$ReleaseAssets + ) + + + $assets = @() + foreach($releaseAsset in $ReleaseAssets) { + $parts = [IO.path]::GetFileNameWithoutExtension($releaseAsset.name).Split("-") + $arch = $parts[-1] + $buildPlatform = [string]::Join("-", $parts[2..($parts.Length-2)]) + + if ($PlatformMap[$buildPlatform]) { + $PlatformMap[$buildPlatform] | ForEach-Object { + $assets += New-AssetItem -Filename $releaseAsset.name ` + -DownloadUrl $releaseAsset.browser_download_url ` + -Arch $arch ` + -Platform $_.platform ` + -PlatformVersion $_.platform_version + } + + } else { + $assets += New-AssetItem -Filename $releaseAsset.name ` + -DownloadUrl $releaseAsset.browser_download_url ` + -Arch $arch ` + -Platform $buildPlatform + } + } + + return $assets +} + +function Get-VersionFromRelease { + param ( + [Parameter(Mandatory)][object]$Release + ) + # Release name can contain additional information after ':' so filter it + [string]$releaseName = $Release.name.Split(':')[0] + [Version]$version = $null + if (![Version]::TryParse($releaseName, [ref]$version)) { + throw "Release '$($Release.id)' has invalid title '$($Release.name)'. It can't be parsed as version. ( $($Release.html_url) )" + } + + return $version +} + +function Build-VersionsManifest { + param ( + [Parameter(Mandatory)][array]$Releases + ) + + $Releases = $Releases | Sort-Object -Property "published_at" -Descending + + $versionsHash = @{} + foreach ($release in $Releases) { + if (($release.draft -eq $true) -or ($release.prerelease -eq $true)) { + continue + } + + [Version]$version = Get-VersionFromRelease $release + $versionKey = $version.ToString() + + if ($versionsHash.ContainsKey($versionKey)) { + continue + } + + $versionsHash.Add($versionKey, [PSCustomObject]@{ + version = $versionKey + stable = $true + release_url = $release.html_url + files = Build-AssetsList $release.assets + }) + } + + # Sort versions by descending + return $versionsHash.Values | Sort-Object -Property @{ Expression = { [Version]$_.version }; Descending = $true } +} + +$gitHubApi = Get-GitHubApi -AccountName $GitHubRepositoryOwner -ProjectName $GitHubRepositoryName -AccessToken $GitHubAccessToken +$releases = $gitHubApi.GetGitHubReleases() +$versionIndex = Build-VersionsManifest $releases +$versionIndex | ConvertTo-Json -Depth 5 | Out-File $OutputFile -Encoding utf8 -Force diff --git a/packages-generation/nix-helpers.psm1 b/packages-generation/nix-helpers.psm1 new file mode 100644 index 0000000..a0d89cc --- /dev/null +++ b/packages-generation/nix-helpers.psm1 @@ -0,0 +1,34 @@ +<# +.SYNOPSIS +Pack folder to *.zip format +#> +function Pack-Zip { + param( + [Parameter(Mandatory=$true)] + [String]$PathToArchive, + [Parameter(Mandatory=$true)] + [String]$ToolZipFile + ) + + Write-Debug "Pack $PathToArchive to $ToolZipFile" + Push-Location -Path $PathToArchive + zip -q -r $ToolZipFile * | Out-Null + Pop-Location +} + +<# +.SYNOPSIS +Unpack *.tar file +#> +function Unpack-TarArchive { + param( + [Parameter(Mandatory=$true)] + [String]$ArchivePath, + [Parameter(Mandatory=$true)] + [String]$OutputDirectory + ) + + Write-Debug "Unpack $ArchivePath to $OutputDirectory" + tar -C $OutputDirectory -xzf $ArchivePath + +} \ No newline at end of file diff --git a/packages-generation/pester-extensions.psm1 b/packages-generation/pester-extensions.psm1 new file mode 100644 index 0000000..46d2a75 --- /dev/null +++ b/packages-generation/pester-extensions.psm1 @@ -0,0 +1,33 @@ +<# +.SYNOPSIS +Pester extension that allows to run command and validate exit code +.EXAMPLE +"python file.py" | Should -ReturnZeroExitCode +#> +function ShouldReturnZeroExitCode { + Param( + [Parameter (Mandatory = $true)] [ValidateNotNullOrEmpty()] + [String]$ActualValue, + [switch]$Negate + ) + + Write-Host "Run command '${ActualValue}'" + Invoke-Expression -Command $ActualValue | ForEach-Object { Write-Host $_ } + $actualExitCode = $LASTEXITCODE + + [bool]$succeeded = $actualExitCode -eq 0 + if ($Negate) { $succeeded = -not $succeeded } + + if (-not $succeeded) + { + $failureMessage = "Command '${ActualValue}' has finished with exit code ${actualExitCode}" + } + + return New-Object PSObject -Property @{ + Succeeded = $succeeded + FailureMessage = $failureMessage + } +} + +Add-AssertionOperator -Name ReturnZeroExitCode ` + -Test $function:ShouldReturnZeroExitCode