├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── NOTES.md ├── README.md ├── SAMPLE-action.ps1 ├── SAMPLE-action.yml ├── _init └── index.js ├── build.ps1 ├── invoke-pwsh.js ├── package-lock.json ├── package.json └── tests └── GitHubActions_tests.ps1 /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | 2 | name: CI 3 | 4 | on: 5 | push: 6 | release: 7 | types: published 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | #runs-on: ubuntu-16.04 14 | steps: 15 | 16 | - name: checkout 17 | uses: actions/checkout@v1 18 | 19 | - name: pester tests 20 | uses: zyborg/pester-tests-report@v1.2.0 21 | with: 22 | include_paths: ./tests/GitHubActions_tests.ps1 23 | exclude_tags: SkipCI 24 | report_name: action_base_tests 25 | report_title: Action Base Tests 26 | gist_name: pwsh-github-action-base_tests.md 27 | github_token: ${{ secrets.GITHUB_TOKEN }} 28 | gist_token: ${{ secrets.GIST_TOKEN }} 29 | gist_badge_label: Tests %ExecutedAt% 30 | 31 | # - name: pester tests manually 32 | # shell: pwsh 33 | # run: | 34 | # $neededModules = @( 35 | # 'Pester' 36 | # 'GitHubActions' 37 | # ) 38 | # $neededModules | % { 39 | # if (-not (Get-Module -ListAvailable $_)) { 40 | # Install-Module $_ -Force 41 | # } 42 | # } 43 | # ./tests/GitHubActions_tests.ps1 44 | 45 | # - name: bundle distributable components 46 | # shell: pwsh 47 | # run: | 48 | # Compress-Archive -DestinationPath ./dist.zip -Path @( 49 | # 'js', 'lib' 50 | # 'SAMPLE-*', 'LICENSE', 'README.md' 51 | # ) 52 | 53 | - name: assemble distributable components 54 | shell: pwsh 55 | run: | 56 | mkdir ./dist 57 | Copy-Item ./_init ./dist/ -Recurse 58 | Copy-Item ./SAMPLE-* ./dist/ 59 | Copy-Item ./LICENSE ./dist/ 60 | Copy-Item ./README.md ./dist/ 61 | 62 | - name: upload distributable artifact 63 | #if: startsWith(github.ref, 'refs/tags/v=') 64 | if: github.event_name == 'release' 65 | uses: actions/upload-artifact@v1 66 | with: 67 | name: dist 68 | path: ./dist 69 | 70 | 71 | ## For testing out tests on Windows 72 | build-on-win: 73 | runs-on: windows-latest 74 | continue-on-error: true 75 | steps: 76 | 77 | - name: checkout 78 | uses: actions/checkout@v1 79 | 80 | - name: pester tests 81 | uses: zyborg/pester-tests-report@v1.2.0 82 | with: 83 | include_paths: ./tests/GitHubActions_tests.ps1 84 | exclude_tags: SkipCI 85 | report_name: action_base_tests-on-win 86 | report_title: Action Base Tests (On Windows) 87 | github_token: ${{ secrets.GITHUB_TOKEN }} 88 | 89 | 90 | publish: 91 | runs-on: ubuntu-latest 92 | needs: build 93 | if: github.event_name == 'release' 94 | steps: 95 | 96 | - name: download distributable artifact 97 | uses: actions/download-artifact@v1 98 | with: 99 | name: dist 100 | 101 | - name: bundle distributable components 102 | shell: pwsh 103 | run: | 104 | cd dist 105 | Compress-Archive -DestinationPath ../pwsh-github-action-base-dist.zip -Path ./* 106 | 107 | - name: attach asset to release 108 | shell: pwsh 109 | env: 110 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 111 | run: | 112 | $tagName = $env:GITHUB_REF -replace 'refs/tags/','' 113 | $githubHeaders = @{ Authorization = "token $($env:GITHUB_TOKEN)" } 114 | $githubRepo = $env:GITHUB_REPOSITORY 115 | $listRelsUrl = "https://api.github.com/repos/$($githubRepo)/releases" 116 | $listRelsResp = Invoke-WebRequest -Headers $githubHeaders $listRelsUrl 117 | 118 | $listRels = $listRelsResp.Content | ConvertFrom-Json 119 | if (-not ($listRels.Count)) { 120 | throw "list releases response did not resolve to any releases" 121 | } 122 | else { 123 | Write-Output "Found [$($listRels.Count)] release(s)." 124 | } 125 | 126 | $thisRel = $listRels | Where-Object { $_.tag_name -eq $tagName } 127 | if (-not $thisRel) { 128 | throw "could not find release for tag [$tagName]" 129 | } 130 | else { 131 | Write-Output "Found release [$($thisRel.tag_name)][$($thisRel.url)]" 132 | } 133 | 134 | $uploadUrl = $thisRel.upload_url.Replace( 135 | '{?name,label}','?name=pwsh-github-action-base-dist.zip') 136 | $uploadHeaders = @{ 137 | "Authorization" = "token $($env:GITHUB_TOKEN)" 138 | "Content-Type" = "application/zip" 139 | } 140 | Write-Output "Adding asset to [$uploadUrl]" 141 | $uploadResp = Invoke-WebRequest -Headers $uploadHeaders $uploadUrl ` 142 | -InFile pwsh-github-action-base-dist.zip 143 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | 332 | 333 | ## Standard Ignores 334 | _IGNORE/ 335 | _TMP/ 336 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Eugene Bekker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NOTES.md: -------------------------------------------------------------------------------- 1 | # NOTES - some impl notes 2 | 3 | You get input from environment variables. More info here: 4 | 5 | * https://help.github.com/en/articles/metadata-syntax-for-github-actions#inputs 6 | * https://help.github.com/en/articles/*orkflow-syntax-for-github-actions#jobsjob_idstepswith 7 | * https://help.github.com/en/articles/virtual-environments-for-github-actions#environment-variables 8 | 9 | You can set output and invoke workflow actions by writing 10 | commands to the console output. More info here: 11 | 12 | * https://help.github.com/en/articles/development-tools-for-github-actions#logging-commands 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pwsh-github-action-base 2 | Base support for implementing GitHub Actions in PowerShell Core 3 | 4 | :star: I appreciate your star, it helps me decide to which OSS projects I should allocate my spare time. 5 | 6 | --- 7 | 8 | [![GitHub Workflow - CI](https://github.com/ebekker/pwsh-github-action-base/workflows/CI/badge.svg)](https://github.com/ebekker/pwsh-github-action-base/actions?workflow=CI) 9 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/ebekker/pwsh-github-action-base)](https://github.com/ebekker/pwsh-github-action-base/releases/latest/download/pwsh-github-action-base-dist.zip) 10 | [![Pester Tests](https://gist.githubusercontent.com/ebekker/bbe0eabece0e4e9c4c8d9e962ed93ea4/raw/pwsh-github-action-base_tests.md_badge.svg)](https://gist.github.com/ebekker/bbe0eabece0e4e9c4c8d9e962ed93ea4) 11 | 12 | --- 13 | 14 | This repository contains a bundle of files to support creating GitHub Actions 15 | in PowerShell Core that can be executed across all the supported platforms 16 | where GitHub Workflows are executed. 17 | 18 | The [distribution](https://github.com/ebekker/pwsh-github-action-base/releases/latest/download/pwsh-github-action-base-dist.zip) 19 | includes a number of base components: 20 | 21 | * **[`_init/index.js`](_init/index.js)** - 22 | The entry point into invoking the Action. 23 | * **[`SAMPLE-action.ps1`](SAMPLE-action.ps1)** - 24 | A sample script implementing a simple Action script demonstrating some 25 | of the features made available from the Core library. 26 | * **[`SAMPLE-action.yml`](SAMPLE-action.yml)** - 27 | A sample Action metadata file that describes various attributes such as a 28 | description, licensing, branding and formal input and output values. 29 | 30 | Additionally, the sample Action shows how to make use of the 31 | [GitHubActions module](https://www.powershellgallery.com/packages/GitHubActions) 32 | to get access to the GH Actions/Workflow environment for input, output 33 | and messaging. More details can be found [below](#optional-support-module) 34 | 35 | ## Required Components 36 | 37 | ### `action.ps1` - The PowerShell Entry Point 38 | 39 | You create an Action by creating a PowerShell script named `action.ps1`, this the 40 | the main entry point into your Action logic. From here you can inspect for, and 41 | assemble inputs and environment variables, call into other processes in the local 42 | environment context, and issue commands to the Action/Workflow context to perform 43 | logging and adjust the state of the job as seen by subsequent Actions and Steps. 44 | 45 | You can also make calls out to external services via APIs, including calling into 46 | the GitHub API. 47 | 48 | ### `action.yml` - The Action Metadata 49 | 50 | As per the GitHub Actions mechanism, you must provide a 51 | [metadata file](https://help.github.com/en/articles/metadata-syntax-for-github-actions) 52 | that describes various attributes about your Action, including any formal inputs 53 | and outputs. You use this metadata file to enumerate any _required_ inputs that 54 | must be provided by a Workflow definition. 55 | 56 | #### `runs` Entry Point Attribute 57 | 58 | The most important attribute in this file for our purposes is the `runs` 59 | setting which has two child settings, `using` and `main`. This attribute 60 | indicates what is the 61 | [_type_](https://help.github.com/en/articles/about-actions#types-of-actions) 62 | of your Action and how to run it. 63 | 64 | There are two main types of Actions, one based on Docker containers and 65 | one based on JavaScript (NodeJS). While Docker Actions give you the ability 66 | to define and _carry_ the entire runtime with you, they are slower to start 67 | and limited to only executing in Linux environments. 68 | 69 | JavaScript Actions however are simpler and more lightweight, and therefore 70 | quicker to start, and they can run on any of the supported platforms 71 | (Linux, Windows, MacOS). They also execute directly in the hosted virtual 72 | machine where the Workflow runs instead of a dedicated container. 73 | 74 | Because of these advantages, this repo hosts a solution that is based on 75 | JavaScript-type Actions. A stub JavaScript script is provided to bootstrap 76 | the Action entry point and then immediately switches over to your provided 77 | PowerShell script. To use this bootstrap script, you need to specify the 78 | following `runs` attribute in your `actions.yml` metadata file: 79 | 80 | ```yaml 81 | runs: 82 | using: node12 83 | main: _init_/index.js 84 | ``` 85 | 86 | This configuration assumes you have placed the bootstrap JavaScript file 87 | in a `_init` subdirectory within your Action root directory. The bootstrap 88 | code also assumes this subdirectory location. If you decide to place it 89 | elsewhere or otherwise rename it, make sure to adjust the metadata file 90 | appropriately, _and_ update the bootstrap script logic to accommodate any 91 | path changes. 92 | 93 | ### `_init/index.js` 94 | 95 | As mentioned above the `_init/index.js` file is a bootstrap JavaScript 96 | file that is used as the initial main entry point into your custom 97 | Action. After starting, it immediately transfers control to your 98 | PowerShell script by invoking the file `action.ps1` in the directory 99 | immediately above the `_init` subdirectory. The invocation is equivalent 100 | to the following command-line call: 101 | 102 | ```pwsh 103 | pwsh -f /full/path/to/action.ps1 104 | ``` 105 | 106 | The working directory is the same as at the start of the bootstrap 107 | script which is the root of the cloned repository of the Workflow 108 | in which the action is being invoked. 109 | 110 | ## Optional Support Module 111 | 112 | In addition to the required components above, you may choose to make use of the 113 | **[`GitHubActions` PowerShell module](https://www.powershellgallery.com/packages/GitHubActions)** 114 | utility script that defines a number of cmdlets that help interact with the 115 | Worklfow/Action environment context in a more natural way for PowerShell 116 | scripts. These cmdlets are adaptations of the JavaScript Actions 117 | [core package](https://github.com/actions/toolkit/tree/master/packages/core) provided in the 118 | [Actions Toolkit](https://github.com/actions/toolkit). See that package 119 | description for details about what it provides 120 | 121 | For details about the counterpart cmdlets, go to the 122 | [docs](https://github.com/ebekker/pwsh-github-action-tools/blob/master/docs/GitHubActions/README.md). 123 | -------------------------------------------------------------------------------- /SAMPLE-action.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | ## 4 | ## This is a sample GitHub Action script written in PowerShell Core. 5 | ## You can write your logic in PWSH to perform GitHub Actions. 6 | ## 7 | 8 | 9 | ## You interface with the Actions/Workflow system by interacting 10 | ## with the environment. The `GitHubActions` module makes this 11 | ## easier and more natural by wrapping up access to the Workflow 12 | ## environment in PowerShell-friendly constructions and idioms 13 | if (-not (Get-Module -ListAvailable GitHubActions)) { 14 | ## Make sure the GH Actions module is installed from the Gallery 15 | Install-Module GitHubActions -Force 16 | } 17 | 18 | ## Load up some common functionality for interacting 19 | ## with the GitHub Actions/Workflow environment 20 | Import-Module GitHubActions 21 | 22 | ## 23 | ## ***** Put your logic here ***** 24 | ## 25 | 26 | ## Pull in some inputs 27 | $salutation = Get-ActionInput salutation -Required 28 | $audience = Get-ActionInput audience 29 | 30 | if (-not $salutation) { 31 | ## We actually specified this input as *required* above so 32 | ## this should never execute, but here is an example value 33 | $salutation = "Hello" 34 | } 35 | if (-not $audience) { 36 | $audience = "World" 37 | } 38 | 39 | $greeting = "$($salutation) $($audience)!" 40 | 41 | ## Persist the greeting in the environment for all subsequent steps 42 | Set-ActionVariable -Name build_greeting -Value greeting 43 | 44 | ## Expose the greeting as an output value of this step instance 45 | Set-ActionOutput -Name greeting -Value $greeting 46 | 47 | ## Write it out to the log for good measure 48 | Write-ActionInfo $greeting 49 | -------------------------------------------------------------------------------- /SAMPLE-action.yml: -------------------------------------------------------------------------------- 1 | 2 | ## This is a SAMPLE metadata file for a GitHub Action. For more info: 3 | ## https://help.github.com/en/articles/metadata-syntax-for-github-actions 4 | 5 | name: action-name 6 | author: your-name 7 | description: What does the action do. 8 | 9 | ## Here you describe your *formal* inputs -- those that are 10 | ## documented and can be displayed by the marketplace. 11 | ## You also use these to identify the *required* inputs. 12 | inputs: 13 | 14 | firstInput: 15 | description: what is this input for? 16 | required: false 17 | 18 | secondInput: 19 | description: what is this input for? 20 | required: false 21 | default: input default value 22 | 23 | ## Here you describe your *formal* outputs. 24 | outputs: 25 | 26 | first_output: 27 | description: what does this output provide? 28 | 29 | second_output: 30 | description: what does this output provide? 31 | 32 | branding: 33 | color: purple 34 | icon: terminal 35 | 36 | ## Even though the Action logic may be implemented 37 | ## in PWSH, we still need a NodeJS entry point 38 | runs: 39 | using: node12 40 | main: _init/index.js 41 | -------------------------------------------------------------------------------- /_init/index.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | /******/ (function(modules, runtime) { // webpackBootstrap 3 | /******/ "use strict"; 4 | /******/ // The module cache 5 | /******/ var installedModules = {}; 6 | /******/ 7 | /******/ // The require function 8 | /******/ function __webpack_require__(moduleId) { 9 | /******/ 10 | /******/ // Check if module is in cache 11 | /******/ if(installedModules[moduleId]) { 12 | /******/ return installedModules[moduleId].exports; 13 | /******/ } 14 | /******/ // Create a new module (and put it into the cache) 15 | /******/ var module = installedModules[moduleId] = { 16 | /******/ i: moduleId, 17 | /******/ l: false, 18 | /******/ exports: {} 19 | /******/ }; 20 | /******/ 21 | /******/ // Execute the module function 22 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 23 | /******/ 24 | /******/ // Flag the module as loaded 25 | /******/ module.l = true; 26 | /******/ 27 | /******/ // Return the exports of the module 28 | /******/ return module.exports; 29 | /******/ } 30 | /******/ 31 | /******/ 32 | /******/ __webpack_require__.ab = __dirname + "/"; 33 | /******/ 34 | /******/ // the startup function 35 | /******/ function startup() { 36 | /******/ // Load entry module and return exports 37 | /******/ return __webpack_require__(157); 38 | /******/ }; 39 | /******/ 40 | /******/ // run startup 41 | /******/ return startup(); 42 | /******/ }) 43 | /************************************************************************/ 44 | /******/ ({ 45 | 46 | /***/ 87: 47 | /***/ (function(module) { 48 | 49 | module.exports = require("os"); 50 | 51 | /***/ }), 52 | 53 | /***/ 129: 54 | /***/ (function(module) { 55 | 56 | module.exports = require("child_process"); 57 | 58 | /***/ }), 59 | 60 | /***/ 157: 61 | /***/ (function(__unusedmodule, __unusedexports, __webpack_require__) { 62 | 63 | 64 | const core = __webpack_require__(580); 65 | const exec = __webpack_require__(871); 66 | 67 | async function run() { 68 | try { 69 | const pwshFolder = __dirname.replace(/[/\\]_init$/, ''); 70 | const pwshScript = `${pwshFolder}/action.ps1` 71 | await exec.exec('pwsh', [ '-f', pwshScript ]); 72 | } catch (error) { 73 | core.setFailed(error.message); 74 | } 75 | } 76 | run(); 77 | 78 | 79 | /***/ }), 80 | 81 | /***/ 159: 82 | /***/ (function(__unusedmodule, exports, __webpack_require__) { 83 | 84 | "use strict"; 85 | 86 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 87 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 88 | return new (P || (P = Promise))(function (resolve, reject) { 89 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 90 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 91 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 92 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 93 | }); 94 | }; 95 | var _a; 96 | Object.defineProperty(exports, "__esModule", { value: true }); 97 | const assert_1 = __webpack_require__(357); 98 | const fs = __webpack_require__(747); 99 | const path = __webpack_require__(622); 100 | _a = fs.promises, exports.chmod = _a.chmod, exports.copyFile = _a.copyFile, exports.lstat = _a.lstat, exports.mkdir = _a.mkdir, exports.readdir = _a.readdir, exports.readlink = _a.readlink, exports.rename = _a.rename, exports.rmdir = _a.rmdir, exports.stat = _a.stat, exports.symlink = _a.symlink, exports.unlink = _a.unlink; 101 | exports.IS_WINDOWS = process.platform === 'win32'; 102 | function exists(fsPath) { 103 | return __awaiter(this, void 0, void 0, function* () { 104 | try { 105 | yield exports.stat(fsPath); 106 | } 107 | catch (err) { 108 | if (err.code === 'ENOENT') { 109 | return false; 110 | } 111 | throw err; 112 | } 113 | return true; 114 | }); 115 | } 116 | exports.exists = exists; 117 | function isDirectory(fsPath, useStat = false) { 118 | return __awaiter(this, void 0, void 0, function* () { 119 | const stats = useStat ? yield exports.stat(fsPath) : yield exports.lstat(fsPath); 120 | return stats.isDirectory(); 121 | }); 122 | } 123 | exports.isDirectory = isDirectory; 124 | /** 125 | * On OSX/Linux, true if path starts with '/'. On Windows, true for paths like: 126 | * \, \hello, \\hello\share, C:, and C:\hello (and corresponding alternate separator cases). 127 | */ 128 | function isRooted(p) { 129 | p = normalizeSeparators(p); 130 | if (!p) { 131 | throw new Error('isRooted() parameter "p" cannot be empty'); 132 | } 133 | if (exports.IS_WINDOWS) { 134 | return (p.startsWith('\\') || /^[A-Z]:/i.test(p) // e.g. \ or \hello or \\hello 135 | ); // e.g. C: or C:\hello 136 | } 137 | return p.startsWith('/'); 138 | } 139 | exports.isRooted = isRooted; 140 | /** 141 | * Recursively create a directory at `fsPath`. 142 | * 143 | * This implementation is optimistic, meaning it attempts to create the full 144 | * path first, and backs up the path stack from there. 145 | * 146 | * @param fsPath The path to create 147 | * @param maxDepth The maximum recursion depth 148 | * @param depth The current recursion depth 149 | */ 150 | function mkdirP(fsPath, maxDepth = 1000, depth = 1) { 151 | return __awaiter(this, void 0, void 0, function* () { 152 | assert_1.ok(fsPath, 'a path argument must be provided'); 153 | fsPath = path.resolve(fsPath); 154 | if (depth >= maxDepth) 155 | return exports.mkdir(fsPath); 156 | try { 157 | yield exports.mkdir(fsPath); 158 | return; 159 | } 160 | catch (err) { 161 | switch (err.code) { 162 | case 'ENOENT': { 163 | yield mkdirP(path.dirname(fsPath), maxDepth, depth + 1); 164 | yield exports.mkdir(fsPath); 165 | return; 166 | } 167 | default: { 168 | let stats; 169 | try { 170 | stats = yield exports.stat(fsPath); 171 | } 172 | catch (err2) { 173 | throw err; 174 | } 175 | if (!stats.isDirectory()) 176 | throw err; 177 | } 178 | } 179 | } 180 | }); 181 | } 182 | exports.mkdirP = mkdirP; 183 | /** 184 | * Best effort attempt to determine whether a file exists and is executable. 185 | * @param filePath file path to check 186 | * @param extensions additional file extensions to try 187 | * @return if file exists and is executable, returns the file path. otherwise empty string. 188 | */ 189 | function tryGetExecutablePath(filePath, extensions) { 190 | return __awaiter(this, void 0, void 0, function* () { 191 | let stats = undefined; 192 | try { 193 | // test file exists 194 | stats = yield exports.stat(filePath); 195 | } 196 | catch (err) { 197 | if (err.code !== 'ENOENT') { 198 | // eslint-disable-next-line no-console 199 | console.log(`Unexpected error attempting to determine if executable file exists '${filePath}': ${err}`); 200 | } 201 | } 202 | if (stats && stats.isFile()) { 203 | if (exports.IS_WINDOWS) { 204 | // on Windows, test for valid extension 205 | const upperExt = path.extname(filePath).toUpperCase(); 206 | if (extensions.some(validExt => validExt.toUpperCase() === upperExt)) { 207 | return filePath; 208 | } 209 | } 210 | else { 211 | if (isUnixExecutable(stats)) { 212 | return filePath; 213 | } 214 | } 215 | } 216 | // try each extension 217 | const originalFilePath = filePath; 218 | for (const extension of extensions) { 219 | filePath = originalFilePath + extension; 220 | stats = undefined; 221 | try { 222 | stats = yield exports.stat(filePath); 223 | } 224 | catch (err) { 225 | if (err.code !== 'ENOENT') { 226 | // eslint-disable-next-line no-console 227 | console.log(`Unexpected error attempting to determine if executable file exists '${filePath}': ${err}`); 228 | } 229 | } 230 | if (stats && stats.isFile()) { 231 | if (exports.IS_WINDOWS) { 232 | // preserve the case of the actual file (since an extension was appended) 233 | try { 234 | const directory = path.dirname(filePath); 235 | const upperName = path.basename(filePath).toUpperCase(); 236 | for (const actualName of yield exports.readdir(directory)) { 237 | if (upperName === actualName.toUpperCase()) { 238 | filePath = path.join(directory, actualName); 239 | break; 240 | } 241 | } 242 | } 243 | catch (err) { 244 | // eslint-disable-next-line no-console 245 | console.log(`Unexpected error attempting to determine the actual case of the file '${filePath}': ${err}`); 246 | } 247 | return filePath; 248 | } 249 | else { 250 | if (isUnixExecutable(stats)) { 251 | return filePath; 252 | } 253 | } 254 | } 255 | } 256 | return ''; 257 | }); 258 | } 259 | exports.tryGetExecutablePath = tryGetExecutablePath; 260 | function normalizeSeparators(p) { 261 | p = p || ''; 262 | if (exports.IS_WINDOWS) { 263 | // convert slashes on Windows 264 | p = p.replace(/\//g, '\\'); 265 | // remove redundant slashes 266 | return p.replace(/\\\\+/g, '\\'); 267 | } 268 | // remove redundant slashes 269 | return p.replace(/\/\/+/g, '/'); 270 | } 271 | // on Mac/Linux, test the execute bit 272 | // R W X R W X R W X 273 | // 256 128 64 32 16 8 4 2 1 274 | function isUnixExecutable(stats) { 275 | return ((stats.mode & 1) > 0 || 276 | ((stats.mode & 8) > 0 && stats.gid === process.getgid()) || 277 | ((stats.mode & 64) > 0 && stats.uid === process.getuid())); 278 | } 279 | //# sourceMappingURL=io-util.js.map 280 | 281 | /***/ }), 282 | 283 | /***/ 200: 284 | /***/ (function(__unusedmodule, exports, __webpack_require__) { 285 | 286 | "use strict"; 287 | 288 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 289 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 290 | return new (P || (P = Promise))(function (resolve, reject) { 291 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 292 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 293 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 294 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 295 | }); 296 | }; 297 | var __importStar = (this && this.__importStar) || function (mod) { 298 | if (mod && mod.__esModule) return mod; 299 | var result = {}; 300 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 301 | result["default"] = mod; 302 | return result; 303 | }; 304 | Object.defineProperty(exports, "__esModule", { value: true }); 305 | const os = __importStar(__webpack_require__(87)); 306 | const events = __importStar(__webpack_require__(614)); 307 | const child = __importStar(__webpack_require__(129)); 308 | const path = __importStar(__webpack_require__(622)); 309 | const io = __importStar(__webpack_require__(460)); 310 | const ioUtil = __importStar(__webpack_require__(159)); 311 | /* eslint-disable @typescript-eslint/unbound-method */ 312 | const IS_WINDOWS = process.platform === 'win32'; 313 | /* 314 | * Class for running command line tools. Handles quoting and arg parsing in a platform agnostic way. 315 | */ 316 | class ToolRunner extends events.EventEmitter { 317 | constructor(toolPath, args, options) { 318 | super(); 319 | if (!toolPath) { 320 | throw new Error("Parameter 'toolPath' cannot be null or empty."); 321 | } 322 | this.toolPath = toolPath; 323 | this.args = args || []; 324 | this.options = options || {}; 325 | } 326 | _debug(message) { 327 | if (this.options.listeners && this.options.listeners.debug) { 328 | this.options.listeners.debug(message); 329 | } 330 | } 331 | _getCommandString(options, noPrefix) { 332 | const toolPath = this._getSpawnFileName(); 333 | const args = this._getSpawnArgs(options); 334 | let cmd = noPrefix ? '' : '[command]'; // omit prefix when piped to a second tool 335 | if (IS_WINDOWS) { 336 | // Windows + cmd file 337 | if (this._isCmdFile()) { 338 | cmd += toolPath; 339 | for (const a of args) { 340 | cmd += ` ${a}`; 341 | } 342 | } 343 | // Windows + verbatim 344 | else if (options.windowsVerbatimArguments) { 345 | cmd += `"${toolPath}"`; 346 | for (const a of args) { 347 | cmd += ` ${a}`; 348 | } 349 | } 350 | // Windows (regular) 351 | else { 352 | cmd += this._windowsQuoteCmdArg(toolPath); 353 | for (const a of args) { 354 | cmd += ` ${this._windowsQuoteCmdArg(a)}`; 355 | } 356 | } 357 | } 358 | else { 359 | // OSX/Linux - this can likely be improved with some form of quoting. 360 | // creating processes on Unix is fundamentally different than Windows. 361 | // on Unix, execvp() takes an arg array. 362 | cmd += toolPath; 363 | for (const a of args) { 364 | cmd += ` ${a}`; 365 | } 366 | } 367 | return cmd; 368 | } 369 | _processLineBuffer(data, strBuffer, onLine) { 370 | try { 371 | let s = strBuffer + data.toString(); 372 | let n = s.indexOf(os.EOL); 373 | while (n > -1) { 374 | const line = s.substring(0, n); 375 | onLine(line); 376 | // the rest of the string ... 377 | s = s.substring(n + os.EOL.length); 378 | n = s.indexOf(os.EOL); 379 | } 380 | strBuffer = s; 381 | } 382 | catch (err) { 383 | // streaming lines to console is best effort. Don't fail a build. 384 | this._debug(`error processing line. Failed with error ${err}`); 385 | } 386 | } 387 | _getSpawnFileName() { 388 | if (IS_WINDOWS) { 389 | if (this._isCmdFile()) { 390 | return process.env['COMSPEC'] || 'cmd.exe'; 391 | } 392 | } 393 | return this.toolPath; 394 | } 395 | _getSpawnArgs(options) { 396 | if (IS_WINDOWS) { 397 | if (this._isCmdFile()) { 398 | let argline = `/D /S /C "${this._windowsQuoteCmdArg(this.toolPath)}`; 399 | for (const a of this.args) { 400 | argline += ' '; 401 | argline += options.windowsVerbatimArguments 402 | ? a 403 | : this._windowsQuoteCmdArg(a); 404 | } 405 | argline += '"'; 406 | return [argline]; 407 | } 408 | } 409 | return this.args; 410 | } 411 | _endsWith(str, end) { 412 | return str.endsWith(end); 413 | } 414 | _isCmdFile() { 415 | const upperToolPath = this.toolPath.toUpperCase(); 416 | return (this._endsWith(upperToolPath, '.CMD') || 417 | this._endsWith(upperToolPath, '.BAT')); 418 | } 419 | _windowsQuoteCmdArg(arg) { 420 | // for .exe, apply the normal quoting rules that libuv applies 421 | if (!this._isCmdFile()) { 422 | return this._uvQuoteCmdArg(arg); 423 | } 424 | // otherwise apply quoting rules specific to the cmd.exe command line parser. 425 | // the libuv rules are generic and are not designed specifically for cmd.exe 426 | // command line parser. 427 | // 428 | // for a detailed description of the cmd.exe command line parser, refer to 429 | // http://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/7970912#7970912 430 | // need quotes for empty arg 431 | if (!arg) { 432 | return '""'; 433 | } 434 | // determine whether the arg needs to be quoted 435 | const cmdSpecialChars = [ 436 | ' ', 437 | '\t', 438 | '&', 439 | '(', 440 | ')', 441 | '[', 442 | ']', 443 | '{', 444 | '}', 445 | '^', 446 | '=', 447 | ';', 448 | '!', 449 | "'", 450 | '+', 451 | ',', 452 | '`', 453 | '~', 454 | '|', 455 | '<', 456 | '>', 457 | '"' 458 | ]; 459 | let needsQuotes = false; 460 | for (const char of arg) { 461 | if (cmdSpecialChars.some(x => x === char)) { 462 | needsQuotes = true; 463 | break; 464 | } 465 | } 466 | // short-circuit if quotes not needed 467 | if (!needsQuotes) { 468 | return arg; 469 | } 470 | // the following quoting rules are very similar to the rules that by libuv applies. 471 | // 472 | // 1) wrap the string in quotes 473 | // 474 | // 2) double-up quotes - i.e. " => "" 475 | // 476 | // this is different from the libuv quoting rules. libuv replaces " with \", which unfortunately 477 | // doesn't work well with a cmd.exe command line. 478 | // 479 | // note, replacing " with "" also works well if the arg is passed to a downstream .NET console app. 480 | // for example, the command line: 481 | // foo.exe "myarg:""my val""" 482 | // is parsed by a .NET console app into an arg array: 483 | // [ "myarg:\"my val\"" ] 484 | // which is the same end result when applying libuv quoting rules. although the actual 485 | // command line from libuv quoting rules would look like: 486 | // foo.exe "myarg:\"my val\"" 487 | // 488 | // 3) double-up slashes that precede a quote, 489 | // e.g. hello \world => "hello \world" 490 | // hello\"world => "hello\\""world" 491 | // hello\\"world => "hello\\\\""world" 492 | // hello world\ => "hello world\\" 493 | // 494 | // technically this is not required for a cmd.exe command line, or the batch argument parser. 495 | // the reasons for including this as a .cmd quoting rule are: 496 | // 497 | // a) this is optimized for the scenario where the argument is passed from the .cmd file to an 498 | // external program. many programs (e.g. .NET console apps) rely on the slash-doubling rule. 499 | // 500 | // b) it's what we've been doing previously (by deferring to node default behavior) and we 501 | // haven't heard any complaints about that aspect. 502 | // 503 | // note, a weakness of the quoting rules chosen here, is that % is not escaped. in fact, % cannot be 504 | // escaped when used on the command line directly - even though within a .cmd file % can be escaped 505 | // by using %%. 506 | // 507 | // the saving grace is, on the command line, %var% is left as-is if var is not defined. this contrasts 508 | // the line parsing rules within a .cmd file, where if var is not defined it is replaced with nothing. 509 | // 510 | // one option that was explored was replacing % with ^% - i.e. %var% => ^%var^%. this hack would 511 | // often work, since it is unlikely that var^ would exist, and the ^ character is removed when the 512 | // variable is used. the problem, however, is that ^ is not removed when %* is used to pass the args 513 | // to an external program. 514 | // 515 | // an unexplored potential solution for the % escaping problem, is to create a wrapper .cmd file. 516 | // % can be escaped within a .cmd file. 517 | let reverse = '"'; 518 | let quoteHit = true; 519 | for (let i = arg.length; i > 0; i--) { 520 | // walk the string in reverse 521 | reverse += arg[i - 1]; 522 | if (quoteHit && arg[i - 1] === '\\') { 523 | reverse += '\\'; // double the slash 524 | } 525 | else if (arg[i - 1] === '"') { 526 | quoteHit = true; 527 | reverse += '"'; // double the quote 528 | } 529 | else { 530 | quoteHit = false; 531 | } 532 | } 533 | reverse += '"'; 534 | return reverse 535 | .split('') 536 | .reverse() 537 | .join(''); 538 | } 539 | _uvQuoteCmdArg(arg) { 540 | // Tool runner wraps child_process.spawn() and needs to apply the same quoting as 541 | // Node in certain cases where the undocumented spawn option windowsVerbatimArguments 542 | // is used. 543 | // 544 | // Since this function is a port of quote_cmd_arg from Node 4.x (technically, lib UV, 545 | // see https://github.com/nodejs/node/blob/v4.x/deps/uv/src/win/process.c for details), 546 | // pasting copyright notice from Node within this function: 547 | // 548 | // Copyright Joyent, Inc. and other Node contributors. All rights reserved. 549 | // 550 | // Permission is hereby granted, free of charge, to any person obtaining a copy 551 | // of this software and associated documentation files (the "Software"), to 552 | // deal in the Software without restriction, including without limitation the 553 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 554 | // sell copies of the Software, and to permit persons to whom the Software is 555 | // furnished to do so, subject to the following conditions: 556 | // 557 | // The above copyright notice and this permission notice shall be included in 558 | // all copies or substantial portions of the Software. 559 | // 560 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 561 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 562 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 563 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 564 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 565 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 566 | // IN THE SOFTWARE. 567 | if (!arg) { 568 | // Need double quotation for empty argument 569 | return '""'; 570 | } 571 | if (!arg.includes(' ') && !arg.includes('\t') && !arg.includes('"')) { 572 | // No quotation needed 573 | return arg; 574 | } 575 | if (!arg.includes('"') && !arg.includes('\\')) { 576 | // No embedded double quotes or backslashes, so I can just wrap 577 | // quote marks around the whole thing. 578 | return `"${arg}"`; 579 | } 580 | // Expected input/output: 581 | // input : hello"world 582 | // output: "hello\"world" 583 | // input : hello""world 584 | // output: "hello\"\"world" 585 | // input : hello\world 586 | // output: hello\world 587 | // input : hello\\world 588 | // output: hello\\world 589 | // input : hello\"world 590 | // output: "hello\\\"world" 591 | // input : hello\\"world 592 | // output: "hello\\\\\"world" 593 | // input : hello world\ 594 | // output: "hello world\\" - note the comment in libuv actually reads "hello world\" 595 | // but it appears the comment is wrong, it should be "hello world\\" 596 | let reverse = '"'; 597 | let quoteHit = true; 598 | for (let i = arg.length; i > 0; i--) { 599 | // walk the string in reverse 600 | reverse += arg[i - 1]; 601 | if (quoteHit && arg[i - 1] === '\\') { 602 | reverse += '\\'; 603 | } 604 | else if (arg[i - 1] === '"') { 605 | quoteHit = true; 606 | reverse += '\\'; 607 | } 608 | else { 609 | quoteHit = false; 610 | } 611 | } 612 | reverse += '"'; 613 | return reverse 614 | .split('') 615 | .reverse() 616 | .join(''); 617 | } 618 | _cloneExecOptions(options) { 619 | options = options || {}; 620 | const result = { 621 | cwd: options.cwd || process.cwd(), 622 | env: options.env || process.env, 623 | silent: options.silent || false, 624 | windowsVerbatimArguments: options.windowsVerbatimArguments || false, 625 | failOnStdErr: options.failOnStdErr || false, 626 | ignoreReturnCode: options.ignoreReturnCode || false, 627 | delay: options.delay || 10000 628 | }; 629 | result.outStream = options.outStream || process.stdout; 630 | result.errStream = options.errStream || process.stderr; 631 | return result; 632 | } 633 | _getSpawnOptions(options, toolPath) { 634 | options = options || {}; 635 | const result = {}; 636 | result.cwd = options.cwd; 637 | result.env = options.env; 638 | result['windowsVerbatimArguments'] = 639 | options.windowsVerbatimArguments || this._isCmdFile(); 640 | if (options.windowsVerbatimArguments) { 641 | result.argv0 = `"${toolPath}"`; 642 | } 643 | return result; 644 | } 645 | /** 646 | * Exec a tool. 647 | * Output will be streamed to the live console. 648 | * Returns promise with return code 649 | * 650 | * @param tool path to tool to exec 651 | * @param options optional exec options. See ExecOptions 652 | * @returns number 653 | */ 654 | exec() { 655 | return __awaiter(this, void 0, void 0, function* () { 656 | // root the tool path if it is unrooted and contains relative pathing 657 | if (!ioUtil.isRooted(this.toolPath) && 658 | (this.toolPath.includes('/') || 659 | (IS_WINDOWS && this.toolPath.includes('\\')))) { 660 | // prefer options.cwd if it is specified, however options.cwd may also need to be rooted 661 | this.toolPath = path.resolve(process.cwd(), this.options.cwd || process.cwd(), this.toolPath); 662 | } 663 | // if the tool is only a file name, then resolve it from the PATH 664 | // otherwise verify it exists (add extension on Windows if necessary) 665 | this.toolPath = yield io.which(this.toolPath, true); 666 | return new Promise((resolve, reject) => { 667 | this._debug(`exec tool: ${this.toolPath}`); 668 | this._debug('arguments:'); 669 | for (const arg of this.args) { 670 | this._debug(` ${arg}`); 671 | } 672 | const optionsNonNull = this._cloneExecOptions(this.options); 673 | if (!optionsNonNull.silent && optionsNonNull.outStream) { 674 | optionsNonNull.outStream.write(this._getCommandString(optionsNonNull) + os.EOL); 675 | } 676 | const state = new ExecState(optionsNonNull, this.toolPath); 677 | state.on('debug', (message) => { 678 | this._debug(message); 679 | }); 680 | const fileName = this._getSpawnFileName(); 681 | const cp = child.spawn(fileName, this._getSpawnArgs(optionsNonNull), this._getSpawnOptions(this.options, fileName)); 682 | const stdbuffer = ''; 683 | if (cp.stdout) { 684 | cp.stdout.on('data', (data) => { 685 | if (this.options.listeners && this.options.listeners.stdout) { 686 | this.options.listeners.stdout(data); 687 | } 688 | if (!optionsNonNull.silent && optionsNonNull.outStream) { 689 | optionsNonNull.outStream.write(data); 690 | } 691 | this._processLineBuffer(data, stdbuffer, (line) => { 692 | if (this.options.listeners && this.options.listeners.stdline) { 693 | this.options.listeners.stdline(line); 694 | } 695 | }); 696 | }); 697 | } 698 | const errbuffer = ''; 699 | if (cp.stderr) { 700 | cp.stderr.on('data', (data) => { 701 | state.processStderr = true; 702 | if (this.options.listeners && this.options.listeners.stderr) { 703 | this.options.listeners.stderr(data); 704 | } 705 | if (!optionsNonNull.silent && 706 | optionsNonNull.errStream && 707 | optionsNonNull.outStream) { 708 | const s = optionsNonNull.failOnStdErr 709 | ? optionsNonNull.errStream 710 | : optionsNonNull.outStream; 711 | s.write(data); 712 | } 713 | this._processLineBuffer(data, errbuffer, (line) => { 714 | if (this.options.listeners && this.options.listeners.errline) { 715 | this.options.listeners.errline(line); 716 | } 717 | }); 718 | }); 719 | } 720 | cp.on('error', (err) => { 721 | state.processError = err.message; 722 | state.processExited = true; 723 | state.processClosed = true; 724 | state.CheckComplete(); 725 | }); 726 | cp.on('exit', (code) => { 727 | state.processExitCode = code; 728 | state.processExited = true; 729 | this._debug(`Exit code ${code} received from tool '${this.toolPath}'`); 730 | state.CheckComplete(); 731 | }); 732 | cp.on('close', (code) => { 733 | state.processExitCode = code; 734 | state.processExited = true; 735 | state.processClosed = true; 736 | this._debug(`STDIO streams have closed for tool '${this.toolPath}'`); 737 | state.CheckComplete(); 738 | }); 739 | state.on('done', (error, exitCode) => { 740 | if (stdbuffer.length > 0) { 741 | this.emit('stdline', stdbuffer); 742 | } 743 | if (errbuffer.length > 0) { 744 | this.emit('errline', errbuffer); 745 | } 746 | cp.removeAllListeners(); 747 | if (error) { 748 | reject(error); 749 | } 750 | else { 751 | resolve(exitCode); 752 | } 753 | }); 754 | if (this.options.input) { 755 | if (!cp.stdin) { 756 | throw new Error('child process missing stdin'); 757 | } 758 | cp.stdin.end(this.options.input); 759 | } 760 | }); 761 | }); 762 | } 763 | } 764 | exports.ToolRunner = ToolRunner; 765 | /** 766 | * Convert an arg string to an array of args. Handles escaping 767 | * 768 | * @param argString string of arguments 769 | * @returns string[] array of arguments 770 | */ 771 | function argStringToArray(argString) { 772 | const args = []; 773 | let inQuotes = false; 774 | let escaped = false; 775 | let arg = ''; 776 | function append(c) { 777 | // we only escape double quotes. 778 | if (escaped && c !== '"') { 779 | arg += '\\'; 780 | } 781 | arg += c; 782 | escaped = false; 783 | } 784 | for (let i = 0; i < argString.length; i++) { 785 | const c = argString.charAt(i); 786 | if (c === '"') { 787 | if (!escaped) { 788 | inQuotes = !inQuotes; 789 | } 790 | else { 791 | append(c); 792 | } 793 | continue; 794 | } 795 | if (c === '\\' && escaped) { 796 | append(c); 797 | continue; 798 | } 799 | if (c === '\\' && inQuotes) { 800 | escaped = true; 801 | continue; 802 | } 803 | if (c === ' ' && !inQuotes) { 804 | if (arg.length > 0) { 805 | args.push(arg); 806 | arg = ''; 807 | } 808 | continue; 809 | } 810 | append(c); 811 | } 812 | if (arg.length > 0) { 813 | args.push(arg.trim()); 814 | } 815 | return args; 816 | } 817 | exports.argStringToArray = argStringToArray; 818 | class ExecState extends events.EventEmitter { 819 | constructor(options, toolPath) { 820 | super(); 821 | this.processClosed = false; // tracks whether the process has exited and stdio is closed 822 | this.processError = ''; 823 | this.processExitCode = 0; 824 | this.processExited = false; // tracks whether the process has exited 825 | this.processStderr = false; // tracks whether stderr was written to 826 | this.delay = 10000; // 10 seconds 827 | this.done = false; 828 | this.timeout = null; 829 | if (!toolPath) { 830 | throw new Error('toolPath must not be empty'); 831 | } 832 | this.options = options; 833 | this.toolPath = toolPath; 834 | if (options.delay) { 835 | this.delay = options.delay; 836 | } 837 | } 838 | CheckComplete() { 839 | if (this.done) { 840 | return; 841 | } 842 | if (this.processClosed) { 843 | this._setResult(); 844 | } 845 | else if (this.processExited) { 846 | this.timeout = setTimeout(ExecState.HandleTimeout, this.delay, this); 847 | } 848 | } 849 | _debug(message) { 850 | this.emit('debug', message); 851 | } 852 | _setResult() { 853 | // determine whether there is an error 854 | let error; 855 | if (this.processExited) { 856 | if (this.processError) { 857 | error = new Error(`There was an error when attempting to execute the process '${this.toolPath}'. This may indicate the process failed to start. Error: ${this.processError}`); 858 | } 859 | else if (this.processExitCode !== 0 && !this.options.ignoreReturnCode) { 860 | error = new Error(`The process '${this.toolPath}' failed with exit code ${this.processExitCode}`); 861 | } 862 | else if (this.processStderr && this.options.failOnStdErr) { 863 | error = new Error(`The process '${this.toolPath}' failed because one or more lines were written to the STDERR stream`); 864 | } 865 | } 866 | // clear the timeout 867 | if (this.timeout) { 868 | clearTimeout(this.timeout); 869 | this.timeout = null; 870 | } 871 | this.done = true; 872 | this.emit('done', error, this.processExitCode); 873 | } 874 | static HandleTimeout(state) { 875 | if (state.done) { 876 | return; 877 | } 878 | if (!state.processClosed && state.processExited) { 879 | const message = `The STDIO streams did not close within ${state.delay / 880 | 1000} seconds of the exit event from process '${state.toolPath}'. This may indicate a child process inherited the STDIO streams and has not yet exited.`; 881 | state._debug(message); 882 | } 883 | state._setResult(); 884 | } 885 | } 886 | //# sourceMappingURL=toolrunner.js.map 887 | 888 | /***/ }), 889 | 890 | /***/ 260: 891 | /***/ (function(__unusedmodule, exports, __webpack_require__) { 892 | 893 | "use strict"; 894 | 895 | var __importStar = (this && this.__importStar) || function (mod) { 896 | if (mod && mod.__esModule) return mod; 897 | var result = {}; 898 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 899 | result["default"] = mod; 900 | return result; 901 | }; 902 | Object.defineProperty(exports, "__esModule", { value: true }); 903 | const os = __importStar(__webpack_require__(87)); 904 | /** 905 | * Commands 906 | * 907 | * Command Format: 908 | * ::name key=value,key=value::message 909 | * 910 | * Examples: 911 | * ::warning::This is the message 912 | * ::set-env name=MY_VAR::some value 913 | */ 914 | function issueCommand(command, properties, message) { 915 | const cmd = new Command(command, properties, message); 916 | process.stdout.write(cmd.toString() + os.EOL); 917 | } 918 | exports.issueCommand = issueCommand; 919 | function issue(name, message = '') { 920 | issueCommand(name, {}, message); 921 | } 922 | exports.issue = issue; 923 | const CMD_STRING = '::'; 924 | class Command { 925 | constructor(command, properties, message) { 926 | if (!command) { 927 | command = 'missing.command'; 928 | } 929 | this.command = command; 930 | this.properties = properties; 931 | this.message = message; 932 | } 933 | toString() { 934 | let cmdStr = CMD_STRING + this.command; 935 | if (this.properties && Object.keys(this.properties).length > 0) { 936 | cmdStr += ' '; 937 | let first = true; 938 | for (const key in this.properties) { 939 | if (this.properties.hasOwnProperty(key)) { 940 | const val = this.properties[key]; 941 | if (val) { 942 | if (first) { 943 | first = false; 944 | } 945 | else { 946 | cmdStr += ','; 947 | } 948 | cmdStr += `${key}=${escapeProperty(val)}`; 949 | } 950 | } 951 | } 952 | } 953 | cmdStr += `${CMD_STRING}${escapeData(this.message)}`; 954 | return cmdStr; 955 | } 956 | } 957 | /** 958 | * Sanitizes an input into a string so it can be passed into issueCommand safely 959 | * @param input input to sanitize into a string 960 | */ 961 | function toCommandValue(input) { 962 | if (input === null || input === undefined) { 963 | return ''; 964 | } 965 | else if (typeof input === 'string' || input instanceof String) { 966 | return input; 967 | } 968 | return JSON.stringify(input); 969 | } 970 | exports.toCommandValue = toCommandValue; 971 | function escapeData(s) { 972 | return toCommandValue(s) 973 | .replace(/%/g, '%25') 974 | .replace(/\r/g, '%0D') 975 | .replace(/\n/g, '%0A'); 976 | } 977 | function escapeProperty(s) { 978 | return toCommandValue(s) 979 | .replace(/%/g, '%25') 980 | .replace(/\r/g, '%0D') 981 | .replace(/\n/g, '%0A') 982 | .replace(/:/g, '%3A') 983 | .replace(/,/g, '%2C'); 984 | } 985 | //# sourceMappingURL=command.js.map 986 | 987 | /***/ }), 988 | 989 | /***/ 357: 990 | /***/ (function(module) { 991 | 992 | module.exports = require("assert"); 993 | 994 | /***/ }), 995 | 996 | /***/ 460: 997 | /***/ (function(__unusedmodule, exports, __webpack_require__) { 998 | 999 | "use strict"; 1000 | 1001 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 1002 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 1003 | return new (P || (P = Promise))(function (resolve, reject) { 1004 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 1005 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 1006 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 1007 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 1008 | }); 1009 | }; 1010 | Object.defineProperty(exports, "__esModule", { value: true }); 1011 | const childProcess = __webpack_require__(129); 1012 | const path = __webpack_require__(622); 1013 | const util_1 = __webpack_require__(669); 1014 | const ioUtil = __webpack_require__(159); 1015 | const exec = util_1.promisify(childProcess.exec); 1016 | /** 1017 | * Copies a file or folder. 1018 | * Based off of shelljs - https://github.com/shelljs/shelljs/blob/9237f66c52e5daa40458f94f9565e18e8132f5a6/src/cp.js 1019 | * 1020 | * @param source source path 1021 | * @param dest destination path 1022 | * @param options optional. See CopyOptions. 1023 | */ 1024 | function cp(source, dest, options = {}) { 1025 | return __awaiter(this, void 0, void 0, function* () { 1026 | const { force, recursive } = readCopyOptions(options); 1027 | const destStat = (yield ioUtil.exists(dest)) ? yield ioUtil.stat(dest) : null; 1028 | // Dest is an existing file, but not forcing 1029 | if (destStat && destStat.isFile() && !force) { 1030 | return; 1031 | } 1032 | // If dest is an existing directory, should copy inside. 1033 | const newDest = destStat && destStat.isDirectory() 1034 | ? path.join(dest, path.basename(source)) 1035 | : dest; 1036 | if (!(yield ioUtil.exists(source))) { 1037 | throw new Error(`no such file or directory: ${source}`); 1038 | } 1039 | const sourceStat = yield ioUtil.stat(source); 1040 | if (sourceStat.isDirectory()) { 1041 | if (!recursive) { 1042 | throw new Error(`Failed to copy. ${source} is a directory, but tried to copy without recursive flag.`); 1043 | } 1044 | else { 1045 | yield cpDirRecursive(source, newDest, 0, force); 1046 | } 1047 | } 1048 | else { 1049 | if (path.relative(source, newDest) === '') { 1050 | // a file cannot be copied to itself 1051 | throw new Error(`'${newDest}' and '${source}' are the same file`); 1052 | } 1053 | yield copyFile(source, newDest, force); 1054 | } 1055 | }); 1056 | } 1057 | exports.cp = cp; 1058 | /** 1059 | * Moves a path. 1060 | * 1061 | * @param source source path 1062 | * @param dest destination path 1063 | * @param options optional. See MoveOptions. 1064 | */ 1065 | function mv(source, dest, options = {}) { 1066 | return __awaiter(this, void 0, void 0, function* () { 1067 | if (yield ioUtil.exists(dest)) { 1068 | let destExists = true; 1069 | if (yield ioUtil.isDirectory(dest)) { 1070 | // If dest is directory copy src into dest 1071 | dest = path.join(dest, path.basename(source)); 1072 | destExists = yield ioUtil.exists(dest); 1073 | } 1074 | if (destExists) { 1075 | if (options.force == null || options.force) { 1076 | yield rmRF(dest); 1077 | } 1078 | else { 1079 | throw new Error('Destination already exists'); 1080 | } 1081 | } 1082 | } 1083 | yield mkdirP(path.dirname(dest)); 1084 | yield ioUtil.rename(source, dest); 1085 | }); 1086 | } 1087 | exports.mv = mv; 1088 | /** 1089 | * Remove a path recursively with force 1090 | * 1091 | * @param inputPath path to remove 1092 | */ 1093 | function rmRF(inputPath) { 1094 | return __awaiter(this, void 0, void 0, function* () { 1095 | if (ioUtil.IS_WINDOWS) { 1096 | // Node doesn't provide a delete operation, only an unlink function. This means that if the file is being used by another 1097 | // program (e.g. antivirus), it won't be deleted. To address this, we shell out the work to rd/del. 1098 | try { 1099 | if (yield ioUtil.isDirectory(inputPath, true)) { 1100 | yield exec(`rd /s /q "${inputPath}"`); 1101 | } 1102 | else { 1103 | yield exec(`del /f /a "${inputPath}"`); 1104 | } 1105 | } 1106 | catch (err) { 1107 | // if you try to delete a file that doesn't exist, desired result is achieved 1108 | // other errors are valid 1109 | if (err.code !== 'ENOENT') 1110 | throw err; 1111 | } 1112 | // Shelling out fails to remove a symlink folder with missing source, this unlink catches that 1113 | try { 1114 | yield ioUtil.unlink(inputPath); 1115 | } 1116 | catch (err) { 1117 | // if you try to delete a file that doesn't exist, desired result is achieved 1118 | // other errors are valid 1119 | if (err.code !== 'ENOENT') 1120 | throw err; 1121 | } 1122 | } 1123 | else { 1124 | let isDir = false; 1125 | try { 1126 | isDir = yield ioUtil.isDirectory(inputPath); 1127 | } 1128 | catch (err) { 1129 | // if you try to delete a file that doesn't exist, desired result is achieved 1130 | // other errors are valid 1131 | if (err.code !== 'ENOENT') 1132 | throw err; 1133 | return; 1134 | } 1135 | if (isDir) { 1136 | yield exec(`rm -rf "${inputPath}"`); 1137 | } 1138 | else { 1139 | yield ioUtil.unlink(inputPath); 1140 | } 1141 | } 1142 | }); 1143 | } 1144 | exports.rmRF = rmRF; 1145 | /** 1146 | * Make a directory. Creates the full path with folders in between 1147 | * Will throw if it fails 1148 | * 1149 | * @param fsPath path to create 1150 | * @returns Promise 1151 | */ 1152 | function mkdirP(fsPath) { 1153 | return __awaiter(this, void 0, void 0, function* () { 1154 | yield ioUtil.mkdirP(fsPath); 1155 | }); 1156 | } 1157 | exports.mkdirP = mkdirP; 1158 | /** 1159 | * Returns path of a tool had the tool actually been invoked. Resolves via paths. 1160 | * If you check and the tool does not exist, it will throw. 1161 | * 1162 | * @param tool name of the tool 1163 | * @param check whether to check if tool exists 1164 | * @returns Promise path to tool 1165 | */ 1166 | function which(tool, check) { 1167 | return __awaiter(this, void 0, void 0, function* () { 1168 | if (!tool) { 1169 | throw new Error("parameter 'tool' is required"); 1170 | } 1171 | // recursive when check=true 1172 | if (check) { 1173 | const result = yield which(tool, false); 1174 | if (!result) { 1175 | if (ioUtil.IS_WINDOWS) { 1176 | throw new Error(`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also verify the file has a valid extension for an executable file.`); 1177 | } 1178 | else { 1179 | throw new Error(`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also check the file mode to verify the file is executable.`); 1180 | } 1181 | } 1182 | } 1183 | try { 1184 | // build the list of extensions to try 1185 | const extensions = []; 1186 | if (ioUtil.IS_WINDOWS && process.env.PATHEXT) { 1187 | for (const extension of process.env.PATHEXT.split(path.delimiter)) { 1188 | if (extension) { 1189 | extensions.push(extension); 1190 | } 1191 | } 1192 | } 1193 | // if it's rooted, return it if exists. otherwise return empty. 1194 | if (ioUtil.isRooted(tool)) { 1195 | const filePath = yield ioUtil.tryGetExecutablePath(tool, extensions); 1196 | if (filePath) { 1197 | return filePath; 1198 | } 1199 | return ''; 1200 | } 1201 | // if any path separators, return empty 1202 | if (tool.includes('/') || (ioUtil.IS_WINDOWS && tool.includes('\\'))) { 1203 | return ''; 1204 | } 1205 | // build the list of directories 1206 | // 1207 | // Note, technically "where" checks the current directory on Windows. From a toolkit perspective, 1208 | // it feels like we should not do this. Checking the current directory seems like more of a use 1209 | // case of a shell, and the which() function exposed by the toolkit should strive for consistency 1210 | // across platforms. 1211 | const directories = []; 1212 | if (process.env.PATH) { 1213 | for (const p of process.env.PATH.split(path.delimiter)) { 1214 | if (p) { 1215 | directories.push(p); 1216 | } 1217 | } 1218 | } 1219 | // return the first match 1220 | for (const directory of directories) { 1221 | const filePath = yield ioUtil.tryGetExecutablePath(directory + path.sep + tool, extensions); 1222 | if (filePath) { 1223 | return filePath; 1224 | } 1225 | } 1226 | return ''; 1227 | } 1228 | catch (err) { 1229 | throw new Error(`which failed with message ${err.message}`); 1230 | } 1231 | }); 1232 | } 1233 | exports.which = which; 1234 | function readCopyOptions(options) { 1235 | const force = options.force == null ? true : options.force; 1236 | const recursive = Boolean(options.recursive); 1237 | return { force, recursive }; 1238 | } 1239 | function cpDirRecursive(sourceDir, destDir, currentDepth, force) { 1240 | return __awaiter(this, void 0, void 0, function* () { 1241 | // Ensure there is not a run away recursive copy 1242 | if (currentDepth >= 255) 1243 | return; 1244 | currentDepth++; 1245 | yield mkdirP(destDir); 1246 | const files = yield ioUtil.readdir(sourceDir); 1247 | for (const fileName of files) { 1248 | const srcFile = `${sourceDir}/${fileName}`; 1249 | const destFile = `${destDir}/${fileName}`; 1250 | const srcFileStat = yield ioUtil.lstat(srcFile); 1251 | if (srcFileStat.isDirectory()) { 1252 | // Recurse 1253 | yield cpDirRecursive(srcFile, destFile, currentDepth, force); 1254 | } 1255 | else { 1256 | yield copyFile(srcFile, destFile, force); 1257 | } 1258 | } 1259 | // Change the mode for the newly created directory 1260 | yield ioUtil.chmod(destDir, (yield ioUtil.stat(sourceDir)).mode); 1261 | }); 1262 | } 1263 | // Buffered file copy 1264 | function copyFile(srcFile, destFile, force) { 1265 | return __awaiter(this, void 0, void 0, function* () { 1266 | if ((yield ioUtil.lstat(srcFile)).isSymbolicLink()) { 1267 | // unlink/re-link it 1268 | try { 1269 | yield ioUtil.lstat(destFile); 1270 | yield ioUtil.unlink(destFile); 1271 | } 1272 | catch (e) { 1273 | // Try to override file permission 1274 | if (e.code === 'EPERM') { 1275 | yield ioUtil.chmod(destFile, '0666'); 1276 | yield ioUtil.unlink(destFile); 1277 | } 1278 | // other errors = it doesn't exist, no work to do 1279 | } 1280 | // Copy over symlink 1281 | const symlinkFull = yield ioUtil.readlink(srcFile); 1282 | yield ioUtil.symlink(symlinkFull, destFile, ioUtil.IS_WINDOWS ? 'junction' : null); 1283 | } 1284 | else if (!(yield ioUtil.exists(destFile)) || force) { 1285 | yield ioUtil.copyFile(srcFile, destFile); 1286 | } 1287 | }); 1288 | } 1289 | //# sourceMappingURL=io.js.map 1290 | 1291 | /***/ }), 1292 | 1293 | /***/ 580: 1294 | /***/ (function(__unusedmodule, exports, __webpack_require__) { 1295 | 1296 | "use strict"; 1297 | 1298 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 1299 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 1300 | return new (P || (P = Promise))(function (resolve, reject) { 1301 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 1302 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 1303 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 1304 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 1305 | }); 1306 | }; 1307 | var __importStar = (this && this.__importStar) || function (mod) { 1308 | if (mod && mod.__esModule) return mod; 1309 | var result = {}; 1310 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 1311 | result["default"] = mod; 1312 | return result; 1313 | }; 1314 | Object.defineProperty(exports, "__esModule", { value: true }); 1315 | const command_1 = __webpack_require__(260); 1316 | const os = __importStar(__webpack_require__(87)); 1317 | const path = __importStar(__webpack_require__(622)); 1318 | /** 1319 | * The code to exit an action 1320 | */ 1321 | var ExitCode; 1322 | (function (ExitCode) { 1323 | /** 1324 | * A code indicating that the action was successful 1325 | */ 1326 | ExitCode[ExitCode["Success"] = 0] = "Success"; 1327 | /** 1328 | * A code indicating that the action was a failure 1329 | */ 1330 | ExitCode[ExitCode["Failure"] = 1] = "Failure"; 1331 | })(ExitCode = exports.ExitCode || (exports.ExitCode = {})); 1332 | //----------------------------------------------------------------------- 1333 | // Variables 1334 | //----------------------------------------------------------------------- 1335 | /** 1336 | * Sets env variable for this action and future actions in the job 1337 | * @param name the name of the variable to set 1338 | * @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify 1339 | */ 1340 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 1341 | function exportVariable(name, val) { 1342 | const convertedVal = command_1.toCommandValue(val); 1343 | process.env[name] = convertedVal; 1344 | command_1.issueCommand('set-env', { name }, convertedVal); 1345 | } 1346 | exports.exportVariable = exportVariable; 1347 | /** 1348 | * Registers a secret which will get masked from logs 1349 | * @param secret value of the secret 1350 | */ 1351 | function setSecret(secret) { 1352 | command_1.issueCommand('add-mask', {}, secret); 1353 | } 1354 | exports.setSecret = setSecret; 1355 | /** 1356 | * Prepends inputPath to the PATH (for this action and future actions) 1357 | * @param inputPath 1358 | */ 1359 | function addPath(inputPath) { 1360 | command_1.issueCommand('add-path', {}, inputPath); 1361 | process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`; 1362 | } 1363 | exports.addPath = addPath; 1364 | /** 1365 | * Gets the value of an input. The value is also trimmed. 1366 | * 1367 | * @param name name of the input to get 1368 | * @param options optional. See InputOptions. 1369 | * @returns string 1370 | */ 1371 | function getInput(name, options) { 1372 | const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || ''; 1373 | if (options && options.required && !val) { 1374 | throw new Error(`Input required and not supplied: ${name}`); 1375 | } 1376 | return val.trim(); 1377 | } 1378 | exports.getInput = getInput; 1379 | /** 1380 | * Sets the value of an output. 1381 | * 1382 | * @param name name of the output to set 1383 | * @param value value to store. Non-string values will be converted to a string via JSON.stringify 1384 | */ 1385 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 1386 | function setOutput(name, value) { 1387 | command_1.issueCommand('set-output', { name }, value); 1388 | } 1389 | exports.setOutput = setOutput; 1390 | /** 1391 | * Enables or disables the echoing of commands into stdout for the rest of the step. 1392 | * Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set. 1393 | * 1394 | */ 1395 | function setCommandEcho(enabled) { 1396 | command_1.issue('echo', enabled ? 'on' : 'off'); 1397 | } 1398 | exports.setCommandEcho = setCommandEcho; 1399 | //----------------------------------------------------------------------- 1400 | // Results 1401 | //----------------------------------------------------------------------- 1402 | /** 1403 | * Sets the action status to failed. 1404 | * When the action exits it will be with an exit code of 1 1405 | * @param message add error issue message 1406 | */ 1407 | function setFailed(message) { 1408 | process.exitCode = ExitCode.Failure; 1409 | error(message); 1410 | } 1411 | exports.setFailed = setFailed; 1412 | //----------------------------------------------------------------------- 1413 | // Logging Commands 1414 | //----------------------------------------------------------------------- 1415 | /** 1416 | * Gets whether Actions Step Debug is on or not 1417 | */ 1418 | function isDebug() { 1419 | return process.env['RUNNER_DEBUG'] === '1'; 1420 | } 1421 | exports.isDebug = isDebug; 1422 | /** 1423 | * Writes debug message to user log 1424 | * @param message debug message 1425 | */ 1426 | function debug(message) { 1427 | command_1.issueCommand('debug', {}, message); 1428 | } 1429 | exports.debug = debug; 1430 | /** 1431 | * Adds an error issue 1432 | * @param message error issue message. Errors will be converted to string via toString() 1433 | */ 1434 | function error(message) { 1435 | command_1.issue('error', message instanceof Error ? message.toString() : message); 1436 | } 1437 | exports.error = error; 1438 | /** 1439 | * Adds an warning issue 1440 | * @param message warning issue message. Errors will be converted to string via toString() 1441 | */ 1442 | function warning(message) { 1443 | command_1.issue('warning', message instanceof Error ? message.toString() : message); 1444 | } 1445 | exports.warning = warning; 1446 | /** 1447 | * Writes info to log with console.log. 1448 | * @param message info message 1449 | */ 1450 | function info(message) { 1451 | process.stdout.write(message + os.EOL); 1452 | } 1453 | exports.info = info; 1454 | /** 1455 | * Begin an output group. 1456 | * 1457 | * Output until the next `groupEnd` will be foldable in this group 1458 | * 1459 | * @param name The name of the output group 1460 | */ 1461 | function startGroup(name) { 1462 | command_1.issue('group', name); 1463 | } 1464 | exports.startGroup = startGroup; 1465 | /** 1466 | * End an output group. 1467 | */ 1468 | function endGroup() { 1469 | command_1.issue('endgroup'); 1470 | } 1471 | exports.endGroup = endGroup; 1472 | /** 1473 | * Wrap an asynchronous function call in a group. 1474 | * 1475 | * Returns the same type as the function itself. 1476 | * 1477 | * @param name The name of the group 1478 | * @param fn The function to wrap in the group 1479 | */ 1480 | function group(name, fn) { 1481 | return __awaiter(this, void 0, void 0, function* () { 1482 | startGroup(name); 1483 | let result; 1484 | try { 1485 | result = yield fn(); 1486 | } 1487 | finally { 1488 | endGroup(); 1489 | } 1490 | return result; 1491 | }); 1492 | } 1493 | exports.group = group; 1494 | //----------------------------------------------------------------------- 1495 | // Wrapper action state 1496 | //----------------------------------------------------------------------- 1497 | /** 1498 | * Saves state for current action, the state can only be retrieved by this action's post job execution. 1499 | * 1500 | * @param name name of the state to store 1501 | * @param value value to store. Non-string values will be converted to a string via JSON.stringify 1502 | */ 1503 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 1504 | function saveState(name, value) { 1505 | command_1.issueCommand('save-state', { name }, value); 1506 | } 1507 | exports.saveState = saveState; 1508 | /** 1509 | * Gets the value of an state set by this action's main execution. 1510 | * 1511 | * @param name name of the state to get 1512 | * @returns string 1513 | */ 1514 | function getState(name) { 1515 | return process.env[`STATE_${name}`] || ''; 1516 | } 1517 | exports.getState = getState; 1518 | //# sourceMappingURL=core.js.map 1519 | 1520 | /***/ }), 1521 | 1522 | /***/ 614: 1523 | /***/ (function(module) { 1524 | 1525 | module.exports = require("events"); 1526 | 1527 | /***/ }), 1528 | 1529 | /***/ 622: 1530 | /***/ (function(module) { 1531 | 1532 | module.exports = require("path"); 1533 | 1534 | /***/ }), 1535 | 1536 | /***/ 669: 1537 | /***/ (function(module) { 1538 | 1539 | module.exports = require("util"); 1540 | 1541 | /***/ }), 1542 | 1543 | /***/ 747: 1544 | /***/ (function(module) { 1545 | 1546 | module.exports = require("fs"); 1547 | 1548 | /***/ }), 1549 | 1550 | /***/ 871: 1551 | /***/ (function(__unusedmodule, exports, __webpack_require__) { 1552 | 1553 | "use strict"; 1554 | 1555 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 1556 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 1557 | return new (P || (P = Promise))(function (resolve, reject) { 1558 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 1559 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 1560 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 1561 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 1562 | }); 1563 | }; 1564 | var __importStar = (this && this.__importStar) || function (mod) { 1565 | if (mod && mod.__esModule) return mod; 1566 | var result = {}; 1567 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 1568 | result["default"] = mod; 1569 | return result; 1570 | }; 1571 | Object.defineProperty(exports, "__esModule", { value: true }); 1572 | const tr = __importStar(__webpack_require__(200)); 1573 | /** 1574 | * Exec a command. 1575 | * Output will be streamed to the live console. 1576 | * Returns promise with return code 1577 | * 1578 | * @param commandLine command to execute (can include additional args). Must be correctly escaped. 1579 | * @param args optional arguments for tool. Escaping is handled by the lib. 1580 | * @param options optional exec options. See ExecOptions 1581 | * @returns Promise exit code 1582 | */ 1583 | function exec(commandLine, args, options) { 1584 | return __awaiter(this, void 0, void 0, function* () { 1585 | const commandArgs = tr.argStringToArray(commandLine); 1586 | if (commandArgs.length === 0) { 1587 | throw new Error(`Parameter 'commandLine' cannot be null or empty.`); 1588 | } 1589 | // Path to tool to execute should be first arg 1590 | const toolPath = commandArgs[0]; 1591 | args = commandArgs.slice(1).concat(args || []); 1592 | const runner = new tr.ToolRunner(toolPath, args, options); 1593 | return runner.exec(); 1594 | }); 1595 | } 1596 | exports.exec = exec; 1597 | //# sourceMappingURL=exec.js.map 1598 | 1599 | /***/ }) 1600 | 1601 | /******/ }); -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [switch]$UpgradePackages 3 | ) 4 | 5 | if ($UpgradePackages) { 6 | & npm upgrade "@actions/core" 7 | & npm upgrade "@actions/exec" 8 | } 9 | 10 | ncc build .\invoke-pwsh.js -o _init 11 | -------------------------------------------------------------------------------- /invoke-pwsh.js: -------------------------------------------------------------------------------- 1 | 2 | const core = require('@actions/core'); 3 | const exec = require('@actions/exec'); 4 | 5 | async function run() { 6 | try { 7 | const pwshFolder = __dirname.replace(/[/\\]_init$/, ''); 8 | const pwshScript = `${pwshFolder}/action.ps1` 9 | await exec.exec('pwsh', [ '-f', pwshScript ]); 10 | } catch (error) { 11 | core.setFailed(error.message); 12 | } 13 | } 14 | run(); 15 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "invoke-pwsh", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@actions/core": { 8 | "version": "1.2.6", 9 | "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz", 10 | "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==" 11 | }, 12 | "@actions/exec": { 13 | "version": "1.0.4", 14 | "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.4.tgz", 15 | "integrity": "sha512-4DPChWow9yc9W3WqEbUj8Nr86xkpyE29ZzWjXucHItclLbEW6jr80Zx4nqv18QL6KK65+cifiQZXvnqgTV6oHw==", 16 | "requires": { 17 | "@actions/io": "^1.0.1" 18 | } 19 | }, 20 | "@actions/io": { 21 | "version": "1.0.2", 22 | "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz", 23 | "integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg==" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "invoke-pwsh", 3 | "version": "1.0.0", 4 | "description": "Invoke PWSH-based GitHub Action.", 5 | "main": "invoke-pwsh.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "EBekker", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@actions/core": "^1.2.6", 13 | "@actions/exec": "^1.0.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/GitHubActions_tests.ps1: -------------------------------------------------------------------------------- 1 | 2 | Import-Module Pester 3 | Import-Module GitHubActions 4 | 5 | Set-Variable -Scope Script -Option Constant -Name EOL -Value ([System.Environment]::NewLine) -ErrorAction Ignore 6 | 7 | Describe 'Set-ActionVariable' { 8 | $testCases = @( 9 | @{ Name = 'varName1' ; Value = 'varValue1' } 10 | @{ Name = 'var name 2'; Value = 'var value 2' } 11 | @{ Name = 'var,name;3'; Value = 'var,value;3' 12 | Expected = "::set-env name=var%2Cname%3B3::var,value;3$EOL" } 13 | ) 14 | It 'Given valid -Name and -Value, and -SkipLocal' -TestCases $testCases { 15 | param($Name, $Value, $Expected) 16 | 17 | if (-not $Expected) { 18 | $Expected = "::set-env name=$($Name)::$($Value)$EOL" 19 | } 20 | 21 | $output = Set-ActionVariable $Name $Value -SkipLocal 22 | $output | Should -Be $Expected 23 | [System.Environment]::GetEnvironmentVariable($Name) | Should -BeNullOrEmpty 24 | } 25 | It 'Given valid -Name and -Value, and NOT -SkipLocal' -TestCases $testCases { 26 | param($Name, $Value, $Expected) 27 | 28 | if (-not $Expected) { 29 | $Expected = "::set-env name=$($Name)::$($Value)$EOL" 30 | } 31 | 32 | Set-ActionVariable $Name $Value | Should -Be $Expected 33 | [System.Environment]::GetEnvironmentVariable($Name) | Should -Be $Value 34 | } 35 | } 36 | 37 | Describe 'Add-ActionSecretMask' { 38 | It 'Given a valid -Secret' { 39 | $secret = 'f00B@r!' 40 | Add-ActionSecretMask $secret | Should -Be "::add-mask::$($secret)$EOL" 41 | } 42 | } 43 | 44 | Describe 'Add-ActionPath' { 45 | It 'Given a valid -Path and -SkipLocal' { 46 | $addPath = '/to/some/path' 47 | $oldPath = [System.Environment]::GetEnvironmentVariable('PATH') 48 | Add-ActionPath $addPath -SkipLocal | Should -Be "::add-path::$($addPath)$EOL" 49 | [System.Environment]::GetEnvironmentVariable('PATH') | Should -Be $oldPath 50 | } 51 | 52 | It 'Given a valid -Path and NOT -SkipLocal' { 53 | $addPath = '/to/some/path' 54 | $oldPath = [System.Environment]::GetEnvironmentVariable('PATH') 55 | $newPath = "$($addPath)$([System.IO.Path]::PathSeparator)$($oldPath)" 56 | Add-ActionPath $addPath | Should -Be "::add-path::$($addPath)$EOL" 57 | [System.Environment]::GetEnvironmentVariable('PATH') | Should -Be $newPath 58 | } 59 | } 60 | 61 | Describe 'Get-ActionInput' { 62 | [System.Environment]::SetEnvironmentVariable('INPUT_INPUT1', 'Value 1') 63 | [System.Environment]::SetEnvironmentVariable('INPUT_INPUT3', 'Value 3') 64 | 65 | $testCases = @( 66 | @{ Name = 'input1' ; Should = @{ Be = $true; ExpectedValue = 'Value 1' } } 67 | @{ Name = 'INPUT1' ; Should = @{ Be = $true; ExpectedValue = 'Value 1' } } 68 | @{ Name = 'Input1' ; Should = @{ Be = $true; ExpectedValue = 'Value 1' } } 69 | @{ Name = 'input2' ; Should = @{ BeNullOrEmpty = $true } } 70 | @{ Name = 'INPUT2' ; Should = @{ BeNullOrEmpty = $true } } 71 | @{ Name = 'Input2' ; Should = @{ BeNullOrEmpty = $true } } 72 | ) 73 | 74 | It 'Given valid -Name' -TestCases $testCases { 75 | param($Name, $Should) 76 | 77 | Get-ActionInput $Name | Should @Should 78 | Get-ActionInput $Name | Should @Should 79 | Get-ActionInput $Name | Should @Should 80 | Get-ActionInput $Name | Should @Should 81 | Get-ActionInput $Name | Should @Should 82 | Get-ActionInput $Name | Should @Should 83 | } 84 | } 85 | 86 | Describe 'Get-ActionInputs' { 87 | [System.Environment]::SetEnvironmentVariable('INPUT_INPUT1', 'Value 1') 88 | [System.Environment]::SetEnvironmentVariable('INPUT_INPUT3', 'Value 3') 89 | 90 | $testCases = @( 91 | @{ Name = 'InPut1' ; Should = @{ Be = $true; ExpectedValue = "Value 1" } } 92 | @{ Name = 'InPut2' ; Should = @{ BeNullOrEmpty = $true } } 93 | @{ Name = 'InPut3' ; Should = @{ Be = $true; ExpectedValue = "Value 3" } } 94 | ) 95 | 96 | ## We skip this test during CI build because we can't be sure of the actual 97 | ## number of INPUT_ environment variables in the real GH Workflow environment 98 | It 'Given 2 predefined inputs' -Tag 'SkipCI' { 99 | $inputs = Get-ActionInputs 100 | $inputs.Count | Should -Be 2 101 | } 102 | 103 | It 'Given 2 predefined inputs, and a -Name in any case' -TestCases $testCases { 104 | param($Name, $Should) 105 | 106 | $inputs = Get-ActionInputs 107 | 108 | $key = $Name 109 | $inputs[$key] | Should @Should 110 | $inputs.$key | Should @Should 111 | $key = $Name.ToUpper() 112 | $inputs[$key] | Should @Should 113 | $inputs.$key | Should @Should 114 | $key = $Name.ToLower() 115 | $inputs[$key] | Should @Should 116 | $inputs.$key | Should @Should 117 | } 118 | } 119 | 120 | Describe 'Set-ActionOuput' { 121 | It 'Given a valid -Name and -Value' { 122 | $output = Set-ActionOutput 'foo_bar' 'foo bar value' 123 | $output | Should -Be "::set-output name=foo_bar::foo bar value$EOL" 124 | } 125 | } 126 | 127 | Describe 'Write-ActionDebug' { 128 | It 'Given a valid -Message' { 129 | $output = Write-ActionDebug 'This is a sample message' 130 | $output | Should -Be "::debug::This is a sample message$EOL" 131 | } 132 | } 133 | 134 | Describe 'Write-ActionError' { 135 | It 'Given a valid -Message' { 136 | $output = Write-ActionError 'This is a sample message' 137 | $output | Should -Be "::error::This is a sample message$EOL" 138 | } 139 | } 140 | 141 | Describe 'Write-ActionWarning' { 142 | It 'Given a valid -Message' { 143 | $output = Write-ActionWarning 'This is a sample message' 144 | $output | Should -Be "::warning::This is a sample message$EOL" 145 | } 146 | } 147 | 148 | Describe 'Write-ActionInfo' { 149 | It 'Given a valid -Message' { 150 | $output = Write-ActionInfo 'This is a sample message' 151 | $output | Should -Be "This is a sample message$EOL" 152 | } 153 | } 154 | 155 | Describe 'Enter-ActionOutputGroup' { 156 | It 'Given a valid -Name' { 157 | $output = Enter-ActionOutputGroup 'Sample Group' 158 | $output | Should -Be "::group::Sample Group$EOL" 159 | } 160 | } 161 | 162 | Describe 'Exit-ActionOutputGroup' { 163 | It 'Given everything is peachy' { 164 | $output = Exit-ActionOutputGroup 165 | $output | Should -Be "::endgroup::$EOL" 166 | } 167 | } 168 | 169 | Describe 'Invoke-ActionWithinOutputGroup' { 170 | It 'Given a valid -Name and -ScriptBlock' { 171 | $output = Invoke-ActionWithinOutputGroup 'Sample Group' { 172 | Write-ActionInfo "Message 1" 173 | Write-ActionInfo "Message 2" 174 | } 175 | 176 | $output | Should -Be @( 177 | "::group::Sample Group$EOL" 178 | "Message 1$EOL" 179 | "Message 2$EOL" 180 | "::endgroup::$EOL" 181 | ) 182 | } 183 | } 184 | --------------------------------------------------------------------------------