commit 4e7c1d2a13be4e5db2ea2182c3f53cbd49d139d0 Author: Maxim Lobanov Date: Thu Apr 23 12:58:18 2020 +0300 Implement script to prepare packages with Node.js 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..4e59ccf --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,52 @@ +## Contributing + +[fork]: https://github.com/actions/node-versions/fork +[pr]: https://github.com/actions/node-versions/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). + +## Code structure + +### Directory structure +``` + +├── azure-pipelines/ +| └──templates/ +├── builders/ +├── helpers/ +├── installers/ +└── tests/ + └──sources/ +``` +- `azure-pipelines*` - contains global YAML definitions for build pipelines. Reusable templates for specific jobs are located in `templates` subfolder. +- `builders` - contains Node.js builder classes and functions. +- `helpers` - contains global helper functions and functions. +- `installers` - contains installation script templates. +- `tests` - contains test scripts. Required tests sources are located in `sources` subfolder. + +\* _We use Azure Pipelines because there are a few features that Actions is still missing, we'll move to Actions as soon as possible_. + +## 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..d0a35b1 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Node.js for Actions +This repository contains the code and scripts that we use to prepare Node.js packages used in [virtual-environments](https://github.com/actions/virtual-environments) and accessible through the [setup-node](https://github.com/actions/setup-node) Action. +File [versions-manifest.json](./versions-manifest.json) contains the list of available and released versions. + +Some versions are pre-installed on [virtual-environments](https://github.com/actions/virtual-environments) images. +More versions will (soon!) be available to install on-the-fly through the [`setup-node`](https://github.com/actions/setup-node) action. + +## Adding new versions +We are trying to prepare packages for new versions of Node.js as soon as they are released. Please open an issue if any versions are missing. + +## 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-pipelines/build-node-packages.yml b/azure-pipelines/build-node-packages.yml new file mode 100644 index 0000000..fdab75e --- /dev/null +++ b/azure-pipelines/build-node-packages.yml @@ -0,0 +1,65 @@ +name: $(date:yyyyMMdd)$(rev:.r)-Node.js-$(VERSION) +trigger: none +pr: + autoCancel: true + branches: + include: + - master + paths: + exclude: + - versions-manifest.json + +stages: +- stage: Build_Node_Darwin + dependsOn: [] + variables: + Platform: darwin + Architecture: x64 + jobs: + - template: /azure-pipelines/templates/build-job.yml + +- stage: Test_Node_Darwin + condition: succeeded() + dependsOn: Build_Node_Darwin + variables: + VmImage: macOS-latest + Platform: darwin + Architecture: x64 + jobs: + - template: /azure-pipelines/templates/test-job.yml + +- stage: Build_Node_Linux + dependsOn: [] + variables: + Platform: linux + Architecture: x64 + jobs: + - template: /azure-pipelines/templates/build-job.yml + +- stage: Test_Node_Linux + condition: succeeded() + dependsOn: Build_Node_Linux + variables: + VmImage: ubuntu-latest + Platform: linux + Architecture: x64 + jobs: + - template: /azure-pipelines/templates/test-job.yml + +- stage: Build_Node_Windows + dependsOn: [] + variables: + Platform: win32 + Architecture: x64 + jobs: + - template: /azure-pipelines/templates/build-job.yml + +- stage: Test_Node_Windows + condition: succeeded() + dependsOn: Build_Node_Windows + variables: + VmImage: windows-latest + Platform: win32 + Architecture: x64 + jobs: + - template: /azure-pipelines/templates/test-job.yml \ No newline at end of file diff --git a/azure-pipelines/templates/build-job.yml b/azure-pipelines/templates/build-job.yml new file mode 100644 index 0000000..db625d8 --- /dev/null +++ b/azure-pipelines/templates/build-job.yml @@ -0,0 +1,29 @@ +jobs: +- job: Build_Node + timeoutInMinutes: 90 + pool: + name: Azure Pipelines + vmImage: ubuntu-latest + steps: + - checkout: self + + - task: PowerShell@2 + displayName: 'Build Node $(Version)' + inputs: + targetType: filePath + filePath: './builders/build-node.ps1' + arguments: '-Version $(Version) -Platform $(Platform) -Architecture $(Architecture)' + + - task: ArchiveFiles@2 + displayName: 'Archive artifact' + inputs: + rootFolderOrFile: '$(Build.BinariesDirectory)' + archiveType: zip + includeRootFolder: false + archiveFile: '$(Build.ArtifactStagingDirectory)/node-$(Version)-$(Platform)-$(Architecture).zip' + + - task: PublishPipelineArtifact@1 + displayName: 'Publish Artifact: Node.js $(Version)' + inputs: + targetPath: '$(Build.ArtifactStagingDirectory)/node-$(Version)-$(Platform)-$(Architecture).zip' + artifactName: 'node-$(Version)-$(Platform)-$(Architecture)' \ No newline at end of file diff --git a/azure-pipelines/templates/test-job.yml b/azure-pipelines/templates/test-job.yml new file mode 100644 index 0000000..4c88389 --- /dev/null +++ b/azure-pipelines/templates/test-job.yml @@ -0,0 +1,78 @@ +jobs: +- job: Test_Node + pool: + name: Azure Pipelines + vmImage: $(VmImage) + steps: + - checkout: self + submodules: true + + - task: PowerShell@2 + displayName: Fully cleanup the toolcache directory + inputs: + TargetType: inline + script: | + $NodeToolcachePath = Join-Path -Path $env:AGENT_TOOLSDIRECTORY -ChildPath "node" + if (Test-Path $NodeToolcachePath) { + Remove-Item -Path $NodeToolcachePath -Recurse -Force + } + + - task: DownloadPipelineArtifact@2 + inputs: + source: 'current' + artifact: 'node-$(Version)-$(Platform)-$(Architecture)' + path: $(Build.ArtifactStagingDirectory) + + - task: ExtractFiles@1 + inputs: + archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/node-$(Version)-$(Platform)-$(Architecture).zip' + destinationFolder: $(Build.BinariesDirectory) + cleanDestinationFolder: false + + - task: PowerShell@2 + displayName: 'Apply build artifact to the local machines' + inputs: + targetType: inline + script: | + if ("$(Platform)" -match 'win32') { powershell ./setup.ps1 } else { sh ./setup.sh } + workingDirectory: '$(Build.BinariesDirectory)' + + - task: NodeTool@0 + displayName: 'Use Node $(Version)' + inputs: + versionSpec: $(Version) + + - task: PowerShell@2 + displayName: 'Wait for the logs' + inputs: + targetType: inline + script: | + Write-Host "Fake step that do nothing" + Write-Host "We need it because log of previous step 'Use Node' is not available here yet." + Write-Host "In testing step (Node.Tests.ps1) we analyze build log of 'Use Node' task" + Write-Host "to determine if Node.js version was consumed from cache and was downloaded" + - task: PowerShell@2 + displayName: 'Run tests' + inputs: + TargetType: inline + script: | + Install-Module Pester -Force -Scope CurrentUser + Import-Module Pester + $pesterParams = @{ + Path="./Node.Tests.ps1"; + Parameters=@{ + Version="$(Version)"; + } + } + Invoke-Pester -Script $pesterParams -OutputFile "test_results.xml" -OutputFormat NUnitXml + workingDirectory: '$(Build.SourcesDirectory)/tests' + + - task: PublishTestResults@2 + displayName: 'Publish test results' + inputs: + testResultsFiles: '*.xml' + testResultsFormat: NUnit + searchFolder: 'tests' + failTaskOnFailedTests: true + testRunTitle: "Node.js $(Version)-$(Platform)" + condition: always() diff --git a/builders/build-node.ps1 b/builders/build-node.ps1 new file mode 100644 index 0000000..7213bd7 --- /dev/null +++ b/builders/build-node.ps1 @@ -0,0 +1,71 @@ +using module "./builders/win-node-builder.psm1" +using module "./builders/nix-node-builder.psm1" + +<# +.SYNOPSIS +Generate Node.js artifact. + +.DESCRIPTION +Main script that creates instance of NodeBuilder and builds of Node.js using specified parameters. + +.PARAMETER Version +Required parameter. The version with which Node.js will be built. + +.PARAMETER Architecture +Optional parameter. The architecture with which Node.js will be built. Using x64 by default. + +.PARAMETER Platform +Required parameter. The platform for which Node.js will be built. + +#> + +param( + [Parameter (Mandatory=$true)][Version] $Version, + [Parameter (Mandatory=$true)][string] $Platform, + [string] $Architecture = "x64" +) + +Import-Module (Join-Path $PSScriptRoot "../helpers" | Join-Path -ChildPath "nix-helpers.psm1") -DisableNameChecking +Import-Module (Join-Path $PSScriptRoot "../helpers" | Join-Path -ChildPath "win-helpers.psm1") -DisableNameChecking + +function Get-NodeBuilder { + <# + .SYNOPSIS + Wrapper for class constructor to simplify importing NodeBuilder. + + .DESCRIPTION + Create instance of NodeBuilder with specified parameters. + + .PARAMETER Version + The version with which Node.js will be built. + + .PARAMETER Platform + The platform for which Node.js will be built. + + .PARAMETER Architecture + The architecture with which Node.js will be built. + + #> + + param ( + [version] $Version, + [string] $Architecture, + [string] $Platform + ) + + $Platform = $Platform.ToLower() + if ($Platform -match 'win32') { + $builder = [WinNodeBuilder]::New($Version, $Platform, $Architecture) + } elseif (($Platform -match 'linux') -or ($Platform -match 'darwin')) { + $builder = [NixNodeBuilder]::New($Version, $Platform, $Architecture) + } else { + Write-Host "##vso[task.logissue type=error;] Invalid platform: $Platform" + exit 1 + } + + return $builder +} + +### Create Node.js builder instance, and build artifact +$Builder = Get-NodeBuilder -Version $Version -Platform $Platform -Architecture $Architecture +$Builder.Build() diff --git a/builders/nix-node-builder.psm1 b/builders/nix-node-builder.psm1 new file mode 100644 index 0000000..c56cd80 --- /dev/null +++ b/builders/nix-node-builder.psm1 @@ -0,0 +1,62 @@ +using module "./builders/node-builder.psm1" + +class NixNodeBuilder : NodeBuilder { + <# + .SYNOPSIS + Ubuntu Node.js builder class. + + .DESCRIPTION + Contains methods that required to build Ubuntu Node.js artifact from sources. Inherited from base NixNodeBuilder. + + .PARAMETER platform + The full name of platform for which Node.js should be built. + + .PARAMETER version + The version of Node.js that should be built. + + #> + + [string] $InstallationTemplateName + [string] $InstallationScriptName + [string] $OutputArtifactName + + NixNodeBuilder( + [version] $version, + [string] $platform, + [string] $architecture + ) : Base($version, $platform, $architecture) { + $this.InstallationTemplateName = "nix-setup-template.sh" + $this.InstallationScriptName = "setup.sh" + $this.OutputArtifactName = "tool.tar.gz" + } + + [uri] GetBinariesUri() { + <# + .SYNOPSIS + Get base Node.js URI and return complete URI for Node.js installation executable. + #> + + $base = $this.GetBaseUri() + return "${base}/v$($this.Version)/node-v$($this.Version)-$($this.Platform)-$($this.Architecture).tar.gz" + } + + [void] ExtractBinaries($archivePath) { + Extract-TarArchive -ArchivePath $archivePath -OutputDirectory $this.ArtifactLocation + } + + [void] CreateInstallationScript() { + <# + .SYNOPSIS + Create Node.js artifact installation script based on template specified in InstallationTemplateName property. + #> + + $installationScriptLocation = New-Item -Path $this.ArtifactLocation -Name $this.InstallationScriptName -ItemType File + $installationTemplateLocation = Join-Path -Path $this.InstallationTemplatesLocation -ChildPath $this.InstallationTemplateName + + $installationTemplateContent = Get-Content -Path $installationTemplateLocation -Raw + $installationTemplateContent = $installationTemplateContent -f $this.Version.ToString(3) + $installationTemplateContent | Out-File -FilePath $installationScriptLocation + + Write-Debug "Done; Installation script location: $installationScriptLocation)" + } +} diff --git a/builders/node-builder.psm1 b/builders/node-builder.psm1 new file mode 100644 index 0000000..21ef28f --- /dev/null +++ b/builders/node-builder.psm1 @@ -0,0 +1,93 @@ +class NodeBuilder { + <# + .SYNOPSIS + Base Node.js builder class. + + .DESCRIPTION + Base Node.js builder class that contains general builder methods. + + .PARAMETER Version + The version of Node.js that should be built. + + .PARAMETER Platform + The platform of Node.js that should be built. + + .PARAMETER Architecture + The architecture with which Node.js should be built. + + .PARAMETER TempFolderLocation + The location of temporary files that will be used during Node.js package generation. Using system BUILD_STAGINGDIRECTORY variable value. + + .PARAMETER ArtifactLocation + The location of generated Node.js artifact. Using system environment BUILD_BINARIESDIRECTORY variable value. + + .PARAMETER InstallationTemplatesLocation + The location of installation script template. Using "installers" folder from current repository. + + #> + + [version] $Version + [string] $Platform + [string] $Architecture + [string] $TempFolderLocation + [string] $ArtifactLocation + [string] $InstallationTemplatesLocation + + NodeBuilder ([version] $version, [string] $platform, [string] $architecture) { + $this.Version = $version + $this.Platform = $platform + $this.Architecture = $architecture + + $this.ArtifactLocation = $env:BUILD_BINARIESDIRECTORY + $this.TempFolderLocation = $env:BUILD_STAGINGDIRECTORY + + $this.InstallationTemplatesLocation = Join-Path -Path $PSScriptRoot -ChildPath "../installers" + } + + [uri] GetBaseUri() { + <# + .SYNOPSIS + Return base URI for Node.js binaries. + #> + + return "https://nodejs.org/download/release" + } + + [string] Download() { + <# + .SYNOPSIS + Download Node.js binaries into artifact location. + #> + + $binariesUri = $this.GetBinariesUri() + $targetFilename = [IO.Path]::GetFileName($binariesUri) + $targetFilepath = Join-Path -Path $this.TempFolderLocation -ChildPath $targetFilename + + Write-Debug "Download binaries from $binariesUri to $targetFilepath" + try { + (New-Object System.Net.WebClient).DownloadFile($binariesUri, $targetFilepath) + } catch { + Write-Host "Error during downloading file from '$binariesUri'" + exit 1 + } + + Write-Debug "Done; Binaries location: $targetFilepath" + return $targetFilepath + } + + [void] Build() { + <# + .SYNOPSIS + Generates Node.js artifact from downloaded binaries. + #> + + Write-Host "Download Node.js $($this.Version) [$($this.Architecture)] executable..." + $binariesArchivePath = $this.Download() + + Write-Host "Unpack binaries to target directory" + $this.ExtractBinaries($binariesArchivePath) + + Write-Host "Create installation script..." + $this.CreateInstallationScript() + } +} diff --git a/builders/win-node-builder.psm1 b/builders/win-node-builder.psm1 new file mode 100644 index 0000000..0246b26 --- /dev/null +++ b/builders/win-node-builder.psm1 @@ -0,0 +1,69 @@ +using module "./builders/node-builder.psm1" + +class WinNodeBuilder : NodeBuilder { + <# + .SYNOPSIS + Ubuntu Node.js builder class. + + .DESCRIPTION + Contains methods that required to build Ubuntu Node.js artifact from sources. Inherited from base NixNodeBuilder. + + .PARAMETER platform + The full name of platform for which Node.js should be built. + + .PARAMETER version + The version of Node.js that should be built. + + #> + + [string] $InstallationTemplateName + [string] $InstallationScriptName + [string] $OutputArtifactName + + WinNodeBuilder( + [version] $version, + [string] $platform, + [string] $architecture + ) : Base($version, $platform, $architecture) { + $this.InstallationTemplateName = "win-setup-template.ps1" + $this.InstallationScriptName = "setup.ps1" + $this.OutputArtifactName = "tool.7z" + } + + [uri] GetBinariesUri() { + <# + .SYNOPSIS + Get base Node.js URI and return complete URI for Node.js installation executable. + #> + + $base = $this.GetBaseUri() + return "${base}/v$($this.Version)/node-v$($this.Version)-win-$($this.Architecture).7z" + } + + [void] ExtractBinaries($archivePath) { + $extractTargetDirectory = Join-Path $this.TempFolderLocation "tempExtract" + Extract-7ZipArchive -ArchivePath $archivePath -OutputDirectory $extractTargetDirectory + $nodeOutputPath = Get-Item $extractTargetDirectory\* | Select-Object -First 1 -ExpandProperty Fullname + Move-Item -Path $nodeOutputPath\* -Destination $this.ArtifactLocation + } + + [void] CreateInstallationScript() { + <# + .SYNOPSIS + Create Node.js artifact installation script based on specified template. + #> + + $installationScriptLocation = New-Item -Path $this.ArtifactLocation -Name $this.InstallationScriptName -ItemType File + $installationTemplateLocation = Join-Path -Path $this.InstallationTemplatesLocation -ChildPath $this.InstallationTemplateName + $installationTemplateContent = Get-Content -Path $installationTemplateLocation -Raw + + $variablesToReplace = @{ + "{{__VERSION__}}" = $this.Version; + "{{__ARCHITECTURE__}}" = $this.Architecture; + } + + $variablesToReplace.keys | ForEach-Object { $installationTemplateContent = $installationTemplateContent.Replace($_, $variablesToReplace[$_]) } + $installationTemplateContent | Out-File -FilePath $installationScriptLocation + Write-Debug "Done; Installation script location: $installationScriptLocation)" + } +} diff --git a/helpers/azure-devops/azure-devops-api.ps1 b/helpers/azure-devops/azure-devops-api.ps1 new file mode 100644 index 0000000..dd2feba --- /dev/null +++ b/helpers/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/helpers/azure-devops/build-info.ps1 b/helpers/azure-devops/build-info.ps1 new file mode 100644 index 0000000..9622d8c --- /dev/null +++ b/helpers/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/helpers/azure-devops/run-ci-builds.ps1 b/helpers/azure-devops/run-ci-builds.ps1 new file mode 100644 index 0000000..bf11295 --- /dev/null +++ b/helpers/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/helpers/github/create-pull-request.ps1 b/helpers/github/create-pull-request.ps1 new file mode 100644 index 0000000..9c01042 --- /dev/null +++ b/helpers/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/helpers/github/git.psm1 b/helpers/github/git.psm1 new file mode 100644 index 0000000..2383a79 --- /dev/null +++ b/helpers/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/helpers/github/github-api.psm1 b/helpers/github/github-api.psm1 new file mode 100644 index 0000000..fef64a5 --- /dev/null +++ b/helpers/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/helpers/nix-helpers.psm1 b/helpers/nix-helpers.psm1 new file mode 100644 index 0000000..6c5bdb4 --- /dev/null +++ b/helpers/nix-helpers.psm1 @@ -0,0 +1,16 @@ +<# +.SYNOPSIS +Unpack *.tar file +#> +function Extract-TarArchive { + param( + [Parameter(Mandatory=$true)] + [String]$ArchivePath, + [Parameter(Mandatory=$true)] + [String]$OutputDirectory + ) + + Write-Debug "Extract $ArchivePath to $OutputDirectory" + tar -C $OutputDirectory -xzf $ArchivePath --strip 1 + +} \ No newline at end of file diff --git a/helpers/packages-generation/generate-versions-manifest.ps1 b/helpers/packages-generation/generate-versions-manifest.ps1 new file mode 100644 index 0000000..7829099 --- /dev/null +++ b/helpers/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 UTF8NoBOM -Force diff --git a/helpers/packages-generation/pester-extensions.psm1 b/helpers/packages-generation/pester-extensions.psm1 new file mode 100644 index 0000000..46d2a75 --- /dev/null +++ b/helpers/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 diff --git a/helpers/win-helpers.psm1 b/helpers/win-helpers.psm1 new file mode 100644 index 0000000..9520fbd --- /dev/null +++ b/helpers/win-helpers.psm1 @@ -0,0 +1,15 @@ +<# +.SYNOPSIS +Unpack *.7z file +#> +function Extract-7ZipArchive { + param( + [Parameter(Mandatory=$true)] + [String]$ArchivePath, + [Parameter(Mandatory=$true)] + [String]$OutputDirectory + ) + + Write-Debug "Extract $ArchivePath to $OutputDirectory" + 7z x $ArchivePath -o"$OutputDirectory" -y | Out-Null +} \ No newline at end of file diff --git a/installers/nix-setup-template.sh b/installers/nix-setup-template.sh new file mode 100644 index 0000000..c24ff22 --- /dev/null +++ b/installers/nix-setup-template.sh @@ -0,0 +1,25 @@ +set -e + +NODE_VERSION={0} + +NODE_TOOLCACHE_PATH=$AGENT_TOOLSDIRECTORY/node +NODE_TOOLCACHE_VERSION_PATH=$NODE_TOOLCACHE_PATH/$NODE_VERSION +NODE_TOOLCACHE_VERSION_ARCH_PATH=$NODE_TOOLCACHE_VERSION_PATH/x64 + +echo "Check if Node.js hostedtoolcache folder exist..." +if [ ! -d $NODE_TOOLCACHE_PATH ]; then + mkdir -p $NODE_TOOLCACHE_PATH +fi + +echo "Delete Node.js $NODE_VERSION if installed" +rm -rf $NODE_TOOLCACHE_VERSION_PATH + +echo "Create Node.js $NODE_VERSION folder" +mkdir -p $NODE_TOOLCACHE_VERSION_ARCH_PATH + +echo "Copy Node.js binaries to hostedtoolcache folder" +cp -R ./* $NODE_TOOLCACHE_VERSION_ARCH_PATH +rm $NODE_TOOLCACHE_VERSION_ARCH_PATH/setup.sh + +echo "Create complete file" +touch $NODE_TOOLCACHE_VERSION_PATH/x64.complete diff --git a/installers/win-setup-template.ps1 b/installers/win-setup-template.ps1 new file mode 100644 index 0000000..7bd3034 --- /dev/null +++ b/installers/win-setup-template.ps1 @@ -0,0 +1,37 @@ +$ErrorActionPreference = "Stop" + +[Version]$Version = "{{__VERSION__}}" +[string]$Architecture = "{{__ARCHITECTURE__}}" +$ArchiveFileName = "tool.7z" +$TempDirectory = Join-Path $env:TEMP "Node" + +$ToolcacheRoot = $env:AGENT_TOOLSDIRECTORY +if ([string]::IsNullOrEmpty($ToolcacheRoot)) { + # GitHub images don't have `AGENT_TOOLSDIRECTORY` variable + $ToolcacheRoot = $env:RUNNER_TOOL_CACHE +} +$NodeToolcachePath = Join-Path -Path $ToolcacheRoot -ChildPath "node" +$NodeToolcacheVersionPath = Join-Path -Path $NodeToolcachePath -ChildPath $Version.ToString() +$NodeToolcacheArchitecturePath = Join-Path $NodeToolcacheVersionPath $Architecture + +Write-Host "Check if Node.js hostedtoolcache folder exist..." +if (-not (Test-Path $NodeToolcachePath)) { + New-Item -ItemType Directory -Path $NodeToolcachePath | Out-Null +} + +Write-Host "Delete Node.js $Version if installed" +if (Test-Path $NodeToolcacheVersionPath) { + Remove-Item $NodeToolcachePath -Recurse -Force | Out-Null +} + +Write-Host "Create Node.js $Version folder" +if (-not (Test-Path $NodeToolcacheArchitecturePath)) { + New-Item -ItemType Directory -Path $NodeToolcacheArchitecturePath | Out-Null +} + +Write-Host "Copy Node.js binaries to hostedtoolcache folder" +Copy-Item -Path * -Destination $NodeToolcacheArchitecturePath +Remove-Item $NodeToolcacheArchitecturePath\setup.ps1 -Force | Out-Null + +Write-Host "Create complete file" +New-Item -ItemType File -Path $NodeToolcacheVersionPath -Name "$Architecture.complete" | Out-Null \ No newline at end of file diff --git a/tests/Node.Tests.ps1 b/tests/Node.Tests.ps1 new file mode 100644 index 0000000..8ff9cb7 --- /dev/null +++ b/tests/Node.Tests.ps1 @@ -0,0 +1,46 @@ +param ( + [Version] [Parameter (Mandatory = $true)] [ValidateNotNullOrEmpty()] + $Version +) + +Import-Module (Join-Path $PSScriptRoot "../helpers/packages-generation/pester-extensions.psm1") + +function Get-UseNodeLogs { + $logsFolderPath = Join-Path -Path $env:AGENT_HOMEDIRECTORY -ChildPath "_diag" | Join-Path -ChildPath "pages" + + $useNodeLogFile = Get-ChildItem -Path $logsFolderPath | Where-Object { + $logContent = Get-Content $_.Fullname -Raw + return $logContent -match "Use Node" + } | Select-Object -First 1 + return $useNodeLogFile.Fullname +} + +Describe "Node.js" { + It "is available" { + "node --version" | Should -ReturnZeroExitCode + } + + It "version is correct" { + $versionOutput = Invoke-Expression "node --version" + $versionOutput | Should -Match $Version + } + + It "is used from tool-cache" { + $nodePath = (Get-Command "node").Path + $nodePath | Should -Not -BeNullOrEmpty + $expectedPath = Join-Path -Path $env:AGENT_TOOLSDIRECTORY -ChildPath "node" + $nodePath.startsWith($expectedPath) | Should -BeTrue -Because "'$nodePath' is not started with '$expectedPath'" + } + + It "cached version is used without downloading" { + # Analyze output of previous steps to check if Node.js was consumed from cache or downloaded + $useNodeLogFile = Get-UseNodeLogs + $useNodeLogFile | Should -Exist + $useNodeLogContent = Get-Content $useNodeLogFile -Raw + $useNodeLogContent | Should -Match "Found tool in cache" + } + + It "Run simple code" { + "node ./simple-test.js" | Should -ReturnZeroExitCode + } +} \ No newline at end of file diff --git a/tests/simple-test.js b/tests/simple-test.js new file mode 100644 index 0000000..7485d3a --- /dev/null +++ b/tests/simple-test.js @@ -0,0 +1,15 @@ +function fibonacci(num){ + var a = 1, b = 0, temp; + + while (num >= 0){ + temp = a; + a = a + b; + b = temp; + num--; + } + + return b; +} + +console.log(fibonacci(7)); +console.log(fibonacci(23)); \ No newline at end of file