1+ <#
2+ . Synopsis
3+ GitHub Action for Turtle
4+ . Description
5+ GitHub Action for Turtle. This will:
6+
7+ * Import Turtle
8+ * If `-Run` is provided, run that script
9+ * Otherwise, unless `-SkipScriptFile` is passed, run all *.Turtle.ps1 files beneath the workflow directory
10+ * If any `-ActionScript` was provided, run scripts from the action path that match a wildcard pattern.
11+
12+ If you will be making changes using the GitHubAPI, you should provide a -GitHubToken
13+ If none is provided, and ENV:GITHUB_TOKEN is set, this will be used instead.
14+ Any files changed can be outputted by the script, and those changes can be checked back into the repo.
15+ Make sure to use the "persistCredentials" option with checkout.
16+ #>
17+
18+ param (
19+ # A PowerShell Script that uses Turtle.
20+ # Any files outputted from the script will be added to the repository.
21+ # If those files have a .Message attached to them, they will be committed with that message.
22+ [string ]
23+ $Run ,
24+
25+ # If set, will not process any files named *.Turtle.ps1
26+ [switch ]
27+ $SkipScriptFile ,
28+
29+ # A list of modules to be installed from the PowerShell gallery before scripts run.
30+ [string []]
31+ $InstallModule ,
32+
33+ # If provided, will commit any remaining changes made to the workspace with this commit message.
34+ [string ]
35+ $CommitMessage ,
36+
37+ # If provided, will checkout a new branch before making the changes.
38+ # If not provided, will use the current branch.
39+ [string ]
40+ $TargetBranch ,
41+
42+ # The name of one or more scripts to run, from this action's path.
43+ [string []]
44+ $ActionScript ,
45+
46+ # The github token to use for requests.
47+ [string ]
48+ $GitHubToken = ' {{ secrets.GITHUB_TOKEN }}' ,
49+
50+ # The user email associated with a git commit. If this is not provided, it will be set to the username@noreply.github.com.
51+ [string ]
52+ $UserEmail ,
53+
54+ # The user name associated with a git commit.
55+ [string ]
56+ $UserName ,
57+
58+ # If set, will not push any changes made to the repository.
59+ # (they will still be committed unless `-NoCommit` is passed)
60+ [switch ]
61+ $NoPush ,
62+
63+ # If set, will not commit any changes made to the repository.
64+ # (this also implies `-NoPush`)
65+ [switch ]
66+ $NoCommit
67+ )
68+
69+ $ErrorActionPreference = ' continue'
70+ " ::group::Parameters" | Out-Host
71+ [PSCustomObject ]$PSBoundParameters | Format-List | Out-Host
72+ " ::endgroup::" | Out-Host
73+
74+ $gitHubEventJson = [IO.File ]::ReadAllText($env: GITHUB_EVENT_PATH )
75+ $gitHubEvent =
76+ if ($env: GITHUB_EVENT_PATH ) {
77+ $gitHubEventJson | ConvertFrom-Json
78+ } else { $null }
79+ " ::group::Parameters" | Out-Host
80+ $gitHubEvent | Format-List | Out-Host
81+ " ::endgroup::" | Out-Host
82+
83+
84+ $anyFilesChanged = $false
85+ $ActionModuleName = ' Turtle'
86+ $actorInfo = $null
87+
88+
89+ $checkDetached = git symbolic- ref - q HEAD
90+ if ($LASTEXITCODE ) {
91+ " ::warning::On detached head, skipping action" | Out-Host
92+ exit 0
93+ }
94+
95+ function InstallActionModule {
96+ param ([string ]$ModuleToInstall )
97+ $moduleInWorkspace = Get-ChildItem - Path $env: GITHUB_WORKSPACE - Recurse - File |
98+ Where-Object Name -eq " $ ( $moduleToInstall ) .psd1" |
99+ Where-Object {
100+ $ (Get-Content $_.FullName - Raw) -match ' ModuleVersion'
101+ }
102+ if (-not $moduleInWorkspace ) {
103+ $availableModules = Get-Module - ListAvailable
104+ if ($availableModules.Name -notcontains $moduleToInstall ) {
105+ Install-Module $moduleToInstall - Scope CurrentUser - Force - AcceptLicense - AllowClobber
106+ }
107+ Import-Module $moduleToInstall - Force - PassThru | Out-Host
108+ } else {
109+ Import-Module $moduleInWorkspace.FullName - Force - PassThru | Out-Host
110+ }
111+ }
112+ function ImportActionModule {
113+ # region -InstallModule
114+ if ($InstallModule ) {
115+ " ::group::Installing Modules" | Out-Host
116+ foreach ($moduleToInstall in $InstallModule ) {
117+ InstallActionModule - ModuleToInstall $moduleToInstall
118+ }
119+ " ::endgroup::" | Out-Host
120+ }
121+ # endregion -InstallModule
122+
123+ if ($env: GITHUB_ACTION_PATH ) {
124+ $LocalModulePath = Join-Path $env: GITHUB_ACTION_PATH " $ActionModuleName .psd1"
125+ if (Test-path $LocalModulePath ) {
126+ Import-Module $LocalModulePath - Force - PassThru | Out-String
127+ } else {
128+ throw " Module '$ActionModuleName ' not found"
129+ }
130+ } elseif (-not (Get-Module $ActionModuleName )) {
131+ throw " Module '$ActionModuleName ' not found"
132+ }
133+
134+ " ::notice title=ModuleLoaded::$ActionModuleName Loaded from Path - $ ( $LocalModulePath ) " | Out-Host
135+ if ($env: GITHUB_STEP_SUMMARY ) {
136+ " # $ ( $ActionModuleName ) " |
137+ Out-File - Append - FilePath $env: GITHUB_STEP_SUMMARY
138+ }
139+ }
140+ function InitializeAction {
141+ # region Custom
142+ # endregion Custom
143+
144+ # Configure git based on the $env:GITHUB_ACTOR
145+ if (-not $UserName ) { $UserName = $env: GITHUB_ACTOR }
146+ if (-not $actorID ) { $actorID = $env: GITHUB_ACTOR_ID }
147+ $actorInfo =
148+ if ($GitHubToken -notmatch ' ^\{{2}' -and $GitHubToken -notmatch ' \}{2}$' ) {
149+ Invoke-RestMethod - Uri " https://api.github.com/user/$actorID " - Headers @ { Authorization = " token $GitHubToken " }
150+ } else {
151+ Invoke-RestMethod - Uri " https://api.github.com/user/$actorID "
152+ }
153+
154+ if (-not $UserEmail ) { $UserEmail = " $UserName @noreply.github.com" }
155+ git config -- global user.email $UserEmail
156+ git config -- global user.name $actorInfo.name
157+
158+ # Pull down any changes
159+ git pull | Out-Host
160+
161+ if ($TargetBranch ) {
162+ " ::notice title=Expanding target branch string $targetBranch " | Out-Host
163+ $TargetBranch = $ExecutionContext.SessionState.InvokeCommand.ExpandString ($TargetBranch )
164+ " ::notice title=Checking out target branch::$targetBranch " | Out-Host
165+ git checkout - b $TargetBranch | Out-Host
166+ git pull | Out-Host
167+ }
168+ }
169+
170+ function InvokeActionModule {
171+ $myScriptStart = [DateTime ]::Now
172+ $myScript = $ExecutionContext.SessionState.PSVariable.Get (" Run" ).Value
173+ if ($myScript ) {
174+ Invoke-Expression - Command $myScript |
175+ . ProcessOutput |
176+ Out-Host
177+ return
178+ }
179+ $myScriptTook = [Datetime ]::Now - $myScriptStart
180+ $MyScriptFilesStart = [DateTime ]::Now
181+
182+ $myScriptList = @ ()
183+ $shouldSkip = $ExecutionContext.SessionState.PSVariable.Get (" SkipScriptFile" ).Value
184+ if ($shouldSkip ) {
185+ return
186+ }
187+ $scriptFiles = @ (
188+ Get-ChildItem - Recurse - Path $env: GITHUB_WORKSPACE |
189+ Where-Object Name -Match " \.$ ( $ActionModuleName ) \.ps1$"
190+ if ($ActionScript ) {
191+ if ($ActionScript -match ' ^\s{0,}/' -and $ActionScript -match ' /\s{0,}$' ) {
192+ $ActionScriptPattern = $ActionScript.Trim (' /' ).Trim() -as [regex ]
193+ if ($ActionScriptPattern ) {
194+ $ActionScriptPattern = [regex ]::new($ActionScript.Trim (' /' ).Trim(), ' IgnoreCase,IgnorePatternWhitespace' , [timespan ]::FromSeconds(0.5 ))
195+ Get-ChildItem - Recurse - Path $env: GITHUB_ACTION_PATH |
196+ Where-Object { $_.Name -Match " \.$ ( $ActionModuleName ) \.ps1$" -and $_.FullName -match $ActionScriptPattern }
197+ }
198+ } else {
199+ Get-ChildItem - Recurse - Path $env: GITHUB_ACTION_PATH |
200+ Where-Object Name -Match " \.$ ( $ActionModuleName ) \.ps1$" |
201+ Where-Object FullName -Like $ActionScript
202+ }
203+ }
204+ ) | Select-Object - Unique
205+ $scriptFiles |
206+ ForEach-Object - Begin {
207+ if ($env: GITHUB_STEP_SUMMARY ) {
208+ " ## $ActionModuleName Scripts" |
209+ Out-File - Append - FilePath $env: GITHUB_STEP_SUMMARY
210+ }
211+ } - Process {
212+ $myScriptList += $_.FullName.Replace ($env: GITHUB_WORKSPACE , ' ' ).TrimStart(' /' )
213+ $myScriptCount ++
214+ $scriptFile = $_
215+ if ($env: GITHUB_STEP_SUMMARY ) {
216+ " ### $ ( $scriptFile.Fullname -replace [Regex ]::Escape($env: GITHUB_WORKSPACE )) " |
217+ Out-File - Append - FilePath $env: GITHUB_STEP_SUMMARY
218+ }
219+ $scriptCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand ($scriptFile.FullName , ' ExternalScript' )
220+ foreach ($requiredModule in $CommandInfo.ScriptBlock.Ast.ScriptRequirements.RequiredModules ) {
221+ if ($requiredModule.Name -and
222+ (-not $requiredModule.MaximumVersion ) -and
223+ (-not $requiredModule.RequiredVersion )
224+ ) {
225+ InstallActionModule $requiredModule.Name
226+ }
227+ }
228+ Push-Location $scriptFile.Directory.Fullname
229+ $scriptFileOutputs = . $scriptCmd
230+ $scriptFileOutputs |
231+ . ProcessOutput |
232+ Out-Host
233+ Pop-Location
234+ }
235+
236+ $MyScriptFilesTook = [Datetime ]::Now - $MyScriptFilesStart
237+ $SummaryOfMyScripts = " $myScriptCount $ActionModuleName scripts took $ ( $MyScriptFilesTook.TotalSeconds ) seconds"
238+ $SummaryOfMyScripts |
239+ Out-Host
240+ if ($env: GITHUB_STEP_SUMMARY ) {
241+ $SummaryOfMyScripts |
242+ Out-File - Append - FilePath $env: GITHUB_STEP_SUMMARY
243+ }
244+ # region Custom
245+ # endregion Custom
246+ }
247+
248+ function OutError {
249+ $anyRuntimeExceptions = $false
250+ foreach ($err in $error ) {
251+ $errParts = @ (
252+ " ::error "
253+ @ (
254+ if ($err.InvocationInfo.ScriptName ) {
255+ " file=$ ( $err.InvocationInfo.ScriptName ) "
256+ }
257+ if ($err.InvocationInfo.ScriptLineNumber -ge 1 ) {
258+ " line=$ ( $err.InvocationInfo.ScriptLineNumber ) "
259+ if ($err.InvocationInfo.OffsetInLine -ge 1 ) {
260+ " col=$ ( $err.InvocationInfo.OffsetInLine ) "
261+ }
262+ }
263+ if ($err.CategoryInfo.Activity ) {
264+ " title=$ ( $err.CategoryInfo.Activity ) "
265+ }
266+ ) -join ' ,'
267+ " ::"
268+ $err.Exception.Message
269+ if ($err.CategoryInfo.Category -eq ' OperationStopped' -and
270+ $err.CategoryInfo.Reason -eq ' RuntimeException' ) {
271+ $anyRuntimeExceptions = $true
272+ }
273+ ) -join ' '
274+ $errParts | Out-Host
275+ if ($anyRuntimeExceptions ) {
276+ exit 1
277+ }
278+ }
279+ }
280+
281+ function PushActionOutput {
282+ if ($anyFilesChanged ) {
283+ " ::notice::$ ( $anyFilesChanged ) Files Changed" | Out-Host
284+ }
285+ if ($CommitMessage -or $anyFilesChanged ) {
286+ if ($CommitMessage ) {
287+ Get-ChildItem $env: GITHUB_WORKSPACE - Recurse |
288+ ForEach-Object {
289+ $gitStatusOutput = git status $_.Fullname - s
290+ if ($gitStatusOutput ) {
291+ git add $_.Fullname
292+ }
293+ }
294+
295+ git commit - m $ExecutionContext.SessionState.InvokeCommand.ExpandString ($CommitMessage )
296+ }
297+
298+ $checkDetached = git symbolic- ref - q HEAD
299+ if (-not $LASTEXITCODE -and -not $NoPush -and -not $noCommit ) {
300+ if ($TargetBranch -and $anyFilesChanged ) {
301+ " ::notice::Pushing Changes to $targetBranch " | Out-Host
302+ git push -- set-upstream origin $TargetBranch
303+ } elseif ($anyFilesChanged ) {
304+ " ::notice::Pushing Changes" | Out-Host
305+ git push
306+ }
307+ " Git Push Output: $ ( $gitPushed | Out-String ) "
308+ } else {
309+ " ::notice::Not pushing changes (on detached head)" | Out-Host
310+ $LASTEXITCODE = 0
311+ exit 0
312+ }
313+ }
314+ }
315+
316+ filter ProcessOutput {
317+ $out = $_
318+ $outItem = Get-Item - Path $out - ErrorAction Ignore
319+ if (-not $outItem -and $out -is [string ]) {
320+ $out | Out-Host
321+ if ($env: GITHUB_STEP_SUMMARY ) {
322+ " > $out " | Out-File - Append - FilePath $env: GITHUB_STEP_SUMMARY
323+ }
324+ return
325+ }
326+ $fullName , $shouldCommit =
327+ if ($out -is [IO.FileInfo ]) {
328+ $out.FullName , (git status $out.Fullname - s)
329+ } elseif ($outItem ) {
330+ $outItem.FullName , (git status $outItem.Fullname - s)
331+ }
332+ if ($shouldCommit -and -not $NoCommit ) {
333+ " $fullName has changed, and should be committed" | Out-Host
334+ git add $fullName
335+ if ($out.Message ) {
336+ git commit - m " $ ( $out.Message ) " | Out-Host
337+ } elseif ($out.CommitMessage ) {
338+ git commit - m " $ ( $out.CommitMessage ) " | Out-Host
339+ } elseif ($gitHubEvent.head_commit.message ) {
340+ git commit - m " $ ( $gitHubEvent.head_commit.message ) " | Out-Host
341+ }
342+ $anyFilesChanged = $true
343+ }
344+ $out
345+ }
346+
347+ . ImportActionModule
348+ . InitializeAction
349+ . InvokeActionModule
350+ . PushActionOutput
351+ . OutError
0 commit comments