Skip to content

Instantly share code, notes, and snippets.

@PanosGreg
Last active March 12, 2026 12:47
Show Gist options
  • Select an option

  • Save PanosGreg/329ea1b9609571e1098124e732523e13 to your computer and use it in GitHub Desktop.

Select an option

Save PanosGreg/329ea1b9609571e1098124e732523e13 to your computer and use it in GitHub Desktop.
Functions for working with GitHub Actions
<#
.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