Last active
March 12, 2026 12:47
-
-
Save PanosGreg/329ea1b9609571e1098124e732523e13 to your computer and use it in GitHub Desktop.
Functions for working with GitHub Actions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| Commands to work with GitHub Actions | |
| This file contains these functions: | |
| - Get-GhaApiVersion | |
| - Get-GhaStep | |
| - Get-Workflow | |
| - Get-WorkflowJob | |
| - Get-WorkflowLog | |
| - Get-WorkflowLogInfo | |
| - Get-WorkflowRun | |
| - Get-WorkflowRunner | |
| - Start-Workflow | |
| - Set-DefaultGhaParameter | |
| - Get-DefaultGhaParameter | |
| - Clear-DefaultGhaParameter | |
| .EXAMPLE | |
| # configure the defaults for all of our commands | |
| Set-DefaultGhaParameter -Owner PanosGreg -Repo tf-lab2026 -Token (gh auth token) -ApiVersion (Get-GhaApiVersion)[0] | |
| # check if the runner is online | |
| Get-WorkflowRunner | |
| # collect all the workflows | |
| $workflows = Get-Workflow | |
| # get all the runs from a sample workflow | |
| $runs = Get-WorkflowRun -Id $workflows[2] | |
| # get the job of a sample workflow run | |
| $job = Get-WorkflowJob -RunId $runs[3].RunId | |
| # finally see the log from that job | |
| Get-WorkflowLog -RunId $job.RunId | |
| # clear the defaults we added earlier on | |
| Clear-DefaultGhaParameter | |
| # to see all the functions from this file | |
| Get-ChildItem Function:\ | where {$_.ScriptBlock.Attributes.TypeId.Name -eq 'IsGhaFunctionAttribute'} | Sort-Object Name | |
| .NOTES | |
| Author: Panos Grigoriadis | |
| Date: 12-Mar-2026 | |
| Gist: https://gist.github.com/PanosGreg/329ea1b9609571e1098124e732523e13 | |
| Disclaimer: | |
| I have not used AI for this gist (for better or for worse). | |
| This means no AI was used to write any of the code here, everything was written by hand. | |
| #> | |
| #Requires -Version 7.5 | |
| class IsGhaFunctionAttribute : Attribute { | |
| # we'll tag all of our functions so we can get them easily | |
| [bool]$HasCommonParams = $true # <-- it implies that the function has the params: Owner,Repo,Token and ApiVersion | |
| IsGhaFunctionAttribute () {} | |
| IsGhaFunctionAttribute ([bool]$HasCommonParams) { | |
| $this.HasCommonParams = $HasCommonParams | |
| } | |
| } | |
| class WorkflowLogInfo { | |
| [int] $StepId | |
| [string] $StepName | |
| [string] $StepLog | |
| [string] $GroupLog | |
| [datetime]$StartAt | |
| [datetime]$EndedAt | |
| WorkflowLogInfo () {} | |
| } | |
| # Note: I should add all the relevant classes for the Get-* commands | |
| function Get-Workflow { | |
| [cmdletbinding()] | |
| [IsGhaFunction()] | |
| param ( | |
| [Parameter(Mandatory)][string]$Owner, | |
| [Parameter(Mandatory)][string]$Repo, | |
| [Parameter(Mandatory)][string]$Token, | |
| [string]$ApiVersion = '2022-11-28' | |
| ) | |
| $params = @{ | |
| Uri = "https://api.github.com/repos/$Owner/$Repo/actions/workflows" | |
| Headers = @{ | |
| Accept = 'application/vnd.github+json' | |
| Authorization = "Bearer $Token" | |
| 'X-GitHub-Api-Version' = $ApiVersion | |
| } | |
| Verbose = $false | |
| } | |
| (Invoke-RestMethod @params).workflows | foreach { | |
| [pscustomobject] @{ | |
| Name = $_.name | |
| Id = $_.id | |
| State = $_.state | |
| Path = $_.path | |
| CreatedAt = $_.created_at | |
| } | |
| } | |
| } | |
| function Get-WorkflowRun { | |
| [cmdletbinding()] | |
| [IsGhaFunction()] | |
| param ( | |
| [Parameter(Mandatory)][string]$Id, | |
| [Parameter(Mandatory)][string]$Owner, | |
| [Parameter(Mandatory)][string]$Repo, | |
| [Parameter(Mandatory)][string]$Token, | |
| [string]$ApiVersion = '2022-11-28' | |
| ) | |
| $params = @{ | |
| Uri = "https://api.github.com/repos/$Owner/$Repo/actions/workflows/$Id/runs" | |
| Headers = @{ | |
| Accept = 'application/vnd.github+json' | |
| Authorization = "Bearer $Token" | |
| 'X-GitHub-Api-Version' = $ApiVersion | |
| } | |
| Verbose = $false | |
| } | |
| (Invoke-RestMethod @params).workflow_runs | foreach { | |
| [pscustomobject] @{ | |
| RunId = $_.id | |
| Name = $_.name | |
| RunNum = $_.run_number -as [int] | |
| Status = $_.status | |
| Result = $_.conclusion | |
| RunAt = $_.run_started_at | |
| RunBy = $_.actor.login | |
| #Url = $_.html_url | |
| } | |
| } | |
| } | |
| function Get-WorkflowJob { | |
| [cmdletbinding()] | |
| [IsGhaFunction()] | |
| param ( | |
| [Parameter(Mandatory)][string]$RunId, | |
| [Parameter(Mandatory)][string]$Owner, | |
| [Parameter(Mandatory)][string]$Repo, | |
| [Parameter(Mandatory)][string]$Token, | |
| [string]$ApiVersion = '2022-11-28' | |
| ) | |
| $params = @{ | |
| Uri = "https://api.github.com/repos/$Owner/$Repo/actions/runs/$RunId/jobs" | |
| Headers = @{ | |
| Accept = 'application/vnd.github+json' | |
| Authorization = "Bearer $Token" | |
| 'X-GitHub-Api-Version' = $ApiVersion | |
| } | |
| Verbose = $false | |
| } | |
| (Invoke-RestMethod @params).jobs | foreach { | |
| $Steps = foreach ($step in $_.steps) { | |
| [pscustomobject]@{ | |
| Name = $step.name | |
| Number = $step.number | |
| Status = $step.status | |
| Result = $step.conclusion | |
| StartedAt = $step.started_at | |
| EndedAt = $step.completed_at | |
| } | |
| } | |
| [pscustomobject] @{ | |
| JobId = $_.id | |
| RunId = $_.run_id | |
| Workflow = $_.workflow_name | |
| Branch = $_.head_branch | |
| Url = $_.html_url | |
| Status = $_.status | |
| Result = $_.conclusion | |
| RunAt = $_.started_at | |
| JobName = $_.name | |
| RunnerName = $_.runner_name | |
| RunnerLabel = $_.labels | |
| Steps = $Steps | |
| } | |
| } | |
| } | |
| function Start-Workflow { | |
| [cmdletbinding()] | |
| [IsGhaFunction()] | |
| param ( | |
| [Parameter(Mandatory)][string]$Id, | |
| [Parameter(Mandatory)][string]$Owner, | |
| [Parameter(Mandatory)][string]$Repo, | |
| [Parameter(Mandatory)][string]$Token, | |
| [string]$Branch = 'main', | |
| [string]$ApiVersion = '2022-11-28' | |
| ) | |
| $params = @{ | |
| Uri = "https://api.github.com/repos/$Owner/$Repo/actions/workflows/$Id/dispatches" | |
| Method = 'Post' | |
| Headers = @{ | |
| Accept = 'application/vnd.github+json' | |
| Authorization = "Bearer $Token" | |
| 'X-GitHub-Api-Version' = $ApiVersion | |
| } | |
| Body = @{ref = $Branch} | ConvertTo-Json # <-- the branch name . I don't have any inputs on this one | |
| Verbose = $false | |
| } | |
| Invoke-RestMethod @params # <-- this returns nothing (it is actually an HTTP Code 204) | |
| } | |
| function Get-WorkflowLog { | |
| [cmdletbinding()] | |
| [IsGhaFunction()] | |
| param ( | |
| [Parameter(Mandatory)][string]$RunId, | |
| [Parameter(Mandatory)][string]$Owner, | |
| [Parameter(Mandatory)][string]$Repo, | |
| [Parameter(Mandatory)][string]$Token, | |
| [string]$ApiVersion = '2022-11-28' | |
| ) | |
| # get the download url for the zip log | |
| $params = @{ | |
| Uri = "https://api.github.com/repos/$Owner/$Repo/actions/runs/$RunId/logs" | |
| Headers = @{ | |
| Accept = 'application/vnd.github+json' | |
| Authorization = "Bearer $Token" | |
| 'X-GitHub-Api-Version' = $ApiVersion | |
| } | |
| Verbose = $false | |
| } | |
| $SmaErr = [System.Management.Automation.ErrorRecord] | |
| $RunLog = Invoke-WebRequest @params -MaximumRedirection 0 -SkipHttpErrorCheck 2>&1 | where {$_ -IsNot $SmaErr} | |
| # Note: we have to disable redirects here, otherwise Invoke-WebRequest follows redirects by default | |
| # we also need to ignore any errors, because this returns a 302 http code, that IWR thinks it failed | |
| # download the zip file | |
| $ZipName = [regex]::Match($RunLog.Headers.Location,'filename=(logs_\d+\.zip)').Groups[1].Value | |
| $ZipFile = Join-Path $env:TEMP $ZipName | |
| $ZipUrl = ($RunLog.Headers.Location | Select-Object -First 1 | Out-String).Trim() | |
| Invoke-WebRequest -Uri $ZipUrl -OutFile $ZipFile # <-- overwrites by default | |
| # decompress the zip file | |
| $UnzipTo = Join-Path $env:TEMP GhaLogs | |
| if (Test-Path $ZipFile) {Expand-Archive -Path $ZipFile -DestinationPath $UnzipTo -Force} | |
| # return the log contents | |
| $TxtFile = Get-ChildItem $UnzipTo\*.txt | |
| $TxtFile | foreach {Get-Content $_ -Raw} | |
| # clean up | |
| Remove-Item $ZipFile | |
| Remove-Item $TxtFile | |
| } | |
| function Get-WorkflowRunner { | |
| [cmdletbinding()] | |
| [IsGhaFunction()] | |
| param ( | |
| [Parameter(Mandatory)][string]$Owner, | |
| [Parameter(Mandatory)][string]$Repo, | |
| [Parameter(Mandatory)][string]$Token, | |
| [string]$ApiVersion = '2022-11-28' | |
| ) | |
| $params = @{ | |
| Uri = "https://api.github.com/repos/$Owner/$Repo/actions/runners" | |
| Headers = @{ | |
| Accept = 'application/vnd.github+json' | |
| Authorization = "Bearer $Token" | |
| 'X-GitHub-Api-Version' = $ApiVersion | |
| } | |
| Verbose = $false | |
| } | |
| (Invoke-RestMethod @params).runners | foreach { | |
| [pscustomobject] @{ | |
| Name = $_.name | |
| Status = $_.status # <-- TODO: if "online" then make it green with VT100 | |
| OS = $_.os | |
| Labels = $_.labels.name # <-- TODO: any custom label, make it blue with VT100 | |
| } # any read-only label, make it gray | |
| } | |
| } | |
| function Get-GhaApiVersion { | |
| [cmdletbinding()] | |
| [outputtype([string[]])] | |
| [IsGhaFunction(HasCommonParams = $false)] | |
| param () | |
| Invoke-RestMethod -Uri 'https://api.github.com/versions' -Headers @{Accept='application/vnd.github+json'} -Verbose:$false | |
| } | |
| function Get-WorkflowLogInfo { | |
| [cmdletbinding()] | |
| [IsGhaFunction()] | |
| param ( | |
| [Parameter(Mandatory)][string]$RunId, | |
| [Parameter(Mandatory)][string]$Owner, | |
| [Parameter(Mandatory)][string]$Repo, | |
| [Parameter(Mandatory)][string]$Token, | |
| [string]$ApiVersion = '2022-11-28' | |
| ) | |
| $Job = Get-WorkflowJob -RunId $RunId -Owner $Owner -Repo $Repo -Token $Token | |
| $Log = Get-WorkflowLog -RunId $RunId -Owner $Owner -Repo $Repo -Token $Token | |
| $Start = ($Log | Select-String -AllMatches -Pattern '##\[group\]').Matches | |
| $End = ($Log | Select-String -AllMatches -Pattern '##\[endgroup\]').Matches | |
| # we assume that start and end found the same number of items | |
| # the following skips the 1st and last steps (these are added by GitHub Actions by default) | |
| # these extra 2 steps are the: Set up job , and the Complete job | |
| # hence why on steps it's $_+1 | |
| 0..($Start.Count-1) | foreach { | |
| $StartIdx = $Start[$_].Index | |
| $EndIdx = $End[$_].Index | |
| $NextStart= $Start[$_+1].Index-1 | |
| if ($NextStart -eq -1) {$NextStart = $Log.Length-1} | |
| $Step = $Job.Steps[$_+1] | |
| [WorkflowLogInfo] @{ | |
| StepId = $_+1 | |
| StepName = $Step.Name | |
| StepLog = $Log[($EndIdx+12)..$NextStart] -join '' | |
| GroupLog = $Log[$StartIdx..($EndIdx+11)] -join '' | |
| StartAt = $Step.StartedAt | |
| EndedAt = $Step.StartedAt | |
| } | |
| } | |
| } | |
| Class GhaFunctionNames : System.Management.Automation.IValidateSetValuesGenerator { | |
| # a custom ValidateSet attribute with all the above functions that have the common parameters: Owner,Repo,Token and ApiVersion | |
| [string[]] GetValidValues() { | |
| $Names = (Get-ChildItem Function:\ | where {$_.ScriptBlock.Attributes.HasCommonParams}).Name | |
| return [string[]]$Names | |
| } | |
| } | |
| function Set-DefaultGhaParameter { | |
| <# | |
| .EXAMPLE | |
| Set-DefaultGhaParameter -Owner PanosGreg -Repo tf-lab2026 -Token (gh auth token) -ApiVersion (Get-GhaApiVersion)[0] | |
| #> | |
| [cmdletbinding()] | |
| [IsGhaFunction()] | |
| param ( | |
| [string]$Owner, | |
| [string]$Repo, | |
| [string]$Token, | |
| [Alias('Version')] | |
| [string]$ApiVersion, | |
| [ValidateSet([GhaFunctionNames])] | |
| [string[]]$FunctionList = (Get-ChildItem Function:\ | where {$_.ScriptBlock.Attributes.HasCommonParams}).Name | |
| ) | |
| # add new or edit existing default parameter values | |
| foreach ($func in $FunctionList) { | |
| if ($Owner) {$Global:PSDefaultParameterValues["${func}:Owner"] = $Owner} | |
| if ($Repo) {$Global:PSDefaultParameterValues["${func}:Repo"] = $Repo} | |
| if ($Token) {$Global:PSDefaultParameterValues["${func}:Token"] = $Token} | |
| if ($ApiVersion) {$Global:PSDefaultParameterValues["${func}:ApiVersion"] = $ApiVersion} | |
| } | |
| } | |
| function Clear-DefaultGhaParameter { | |
| [cmdletbinding()] | |
| [IsGhaFunction(HasCommonParams = $false)] | |
| param () | |
| $FunctionList = (Get-ChildItem Function:\ | where {$_.ScriptBlock.Attributes.HasCommonParams}).Name | |
| foreach ($func in $FunctionList) { | |
| $Global:PSDefaultParameterValues.Remove("${func}:Owner") | |
| $Global:PSDefaultParameterValues.Remove("${func}:Repo") | |
| $Global:PSDefaultParameterValues.Remove("${func}:Token") | |
| $Global:PSDefaultParameterValues.Remove("${func}:ApiVersion") | |
| } | |
| } | |
| function Get-DefaultGhaParameter { | |
| [cmdletbinding()] | |
| [IsGhaFunction(HasCommonParams = $false)] | |
| param ( | |
| [ValidateSet([GhaFunctionNames])] | |
| [string[]]$FunctionList = (Get-ChildItem Function:\ | where {$_.ScriptBlock.Attributes.HasCommonParams}).Name | |
| ) | |
| $out = foreach ($func in $FunctionList) { | |
| [pscustomobject] @{ | |
| PSTypeName = 'DefaultGhaParameter' | |
| Function = $func | |
| Owner = $Global:PSDefaultParameterValues["${func}:Owner"] | |
| Repo = $Global:PSDefaultParameterValues["${func}:Repo"] | |
| Token = $Global:PSDefaultParameterValues["${func}:Token"] | |
| Version = $Global:PSDefaultParameterValues["${func}:ApiVersion"] | |
| } | |
| } | |
| $Props = [string[]]('Function','Owner','Repo','Version') | |
| $DDPS = [Management.Automation.PSPropertySet]::new('DefaultDisplayPropertySet',$Props) | |
| $Std = [Management.Automation.PSMemberInfo[]]($DDPS) | |
| $out | Add-Member MemberSet PSStandardMembers $Std -PassThru | |
| } | |
| function Get-GhaStep { | |
| <# | |
| .SYNOPSIS | |
| Get the Steps of the currently running Job, of the GitHub Actions Workflow | |
| This is meant to be executed from within the runner, while the workflow is running. | |
| #> | |
| [cmdletbinding()] | |
| [IsGhaFunction(HasCommonParams = $false)] | |
| param () | |
| $Path = Split-Path (Get-Service actions.runner.*).BinaryPathName.Replace('"',$null) | |
| $File = Get-ChildItem $Path\..\_diag\Worker_* | Sort-Object LastWriteTime -Descending | select -First 1 | |
| $text = Get-Content -Path $File.FullName | |
| $total = ($text | Select-String 'Total job steps: (\d+)').Matches.Groups[1].Value -as [int] | |
| $steps = $text | Select-String "Processing step: DisplayName='(.+)'" | foreach {$_.Matches.Groups[1].Value} | |
| $i=0 ; $steps | foreach { | |
| if ($env:GITHUB_WORKFLOW_REF) {$yaml = Split-Path $env:GITHUB_WORKFLOW_REF.Split('@')[0] -Leaf} | |
| else {$yaml = $null} | |
| [pscustomobject] @{ | |
| PSTypeName = 'GithubActions.Step' | |
| File = $yaml | |
| Workflow = $env:GITHUB_WORKFLOW | |
| Job = $env:GITHUB_JOB | |
| Step = $_ | |
| Index = ++$i | |
| Count = $total | |
| IsFirst = $i -eq 1 | |
| IsLast = $i -eq $total | |
| JobRun = $env:GITHUB_RUN_NUMBER | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment