GitHub Actions
pyTooling.CI.GitHub models a GitHub Actions workflow run:
from pyTooling.CI.GitHub import Pipeline
pipeline = Pipeline.FromJSON(run, jobs) # the REST API's two payloads
print(f"{pipeline.Name} #{pipeline.RunNumber}: {pipeline.Conclusion.name} in {pipeline.Duration} s")
for job in pipeline.IterateJobs():
print(f" {job.Name:<20} queued {job.QueueDuration} s, ran {job.Duration} s on {job.Labels}")
The Tree
PipelineGroup every run started for one commit
+-- Pipeline a workflow run
+-- Workflow a called (reusable) workflow
| +-- Workflow a workflow called by that workflow
| +-- Matrix a matrix
| | +-- MatrixJob one instance it produced
| +-- Job
+-- Matrix
+-- Job a job that ran on a runner
+-- Step a step of that job
Every element knows its Parent, and holds a reference to the workflow run it
belongs to in Pipeline - so reaching the run from any depth costs no walk.
Iterating an element yields what it contains one level down: a workflow yields its jobs, its matrices and the
workflows it calls, a matrix its instances, and a job its steps. To reach every job below a workflow at once - those
of its matrices and of the workflows it calls included - use
IterateJobs():
for element in pipeline: # one level: jobs, matrices, called workflows
print(f"{type(element).__name__}: {element}")
for job in pipeline.IterateJobs(): # every job below the run, at any depth
print(job.QualifiedName)
An element is placed in its group under its own name - a called workflow and a matrix as that key of
Workflows respectively Matrices, a job by
the name it reports - so in is asked for that name:
"UnitTesting" in pipeline # a called workflow, a matrix or a job of the run
"Unit Tests (ubuntu-26.04)" in matrix # an instance carries the values telling it from its siblings
"Checkout" in job # a step
Which container an element really sits in is a different question, and Parent
answers it without a search:
workflow.Jobs[0].Parent is workflow # True
A called workflow and a matrix are not elements GitHub reports. It encodes both in a job’s name -
Caller / Jobfor a called workflow,Job (ubuntu-26.04, 3.14)for a matrix instance - andFromJSON()reads the name back into the tree.Pathnames the workflow’s YAML file andWorkflowIDthe workflow it belongs to, so a run can be traced back to the file that started it.
Caution
A called workflow has none of that. Its name, taken from the prefix of a job’s name, is the only thing
GitHub reports about it - not its YAML file, not the @ref the caller pinned it at, and not the repository it
lives in when that differs from the caller’s. Neither the runs nor the jobs payload holds any of it, so filling
it in means reading the caller’s workflow file and resolving its uses: entries, which is a different source
than this model reads. Issue #408 describes what is
missing and how it could be supplied.
A reusable workflow may call another, and a job’s name carries the whole caller chain, so the tree nests as deeply as the chain is long - to GitHub’s limit of four levels and beyond, should it ever be raised. A level is shared rather than repeated:
A / B / C / DeepandA / B / OtherputOtherbesideCbelow the sameB.Neither level reports times, so
JobGroupderives them: a group begins with its earliest job and ends with its latest, and has no end while a job below it is still running.Because the name is taken apart,
QualifiedNameMixinputs it back together - a job belowCallerreportsCaller / Build (ubuntu-26.04)as itsQualifiedNamewhileNamestaysBuild, so a report can name a job the way the service does without walking the tree itself. AJoband aWorkfloware named that way; aMatrixisn’t, since GitHub reports no name for it.The bracketed suffix is a convention of GitHub’s own interface rather than a field, so a job genuinely named
Build (fast)and produced by no matrix is indistinguishable from one that was - it becomes a matrix of one instance. A job whose workflow sets its ownname:carries no values at all, and its matrix stays invisible.
Strings Become Enumerations
status, conclusion and event arrive as text. Status,
Conclusion and Event turn them into members, so a value
GitHub doesn’t document raises GitHubError instead of quietly matching no comparison:
if job.Conclusion is Conclusion.TimedOut: # not: job["conclusion"] == "timeout"
...
Timestamps are parsed once, and a timestamp without a time zone is read as UTC, so every time of a run can be compared with every other.
Several Pipelines per Commit
A push starts one run per workflow file whose triggers match, so a commit has several pipelines -
PipelineGroup holds them.
A run started at a tag is in that group as well and is not the same thing: it carries the same head_sha, so
the API can’t separate it, but it was started later and for another reason - a release pipeline tags its own commit,
and the run at that tag publishes the release. GitHub reports the tag’s name in head_branch -
GitReference - with
no field saying it is a tag, so ByGitReference() is what separates them:
group = PipelineGroup.FromJSON(runs) # GET .../actions/runs?head_sha=...
for reference, pipelines in group.ByGitReference().items():
print(f"{reference}: {len(pipelines)} pipeline(s)")
# main: 6 pipeline(s)
# v1.6.0: 1 pipeline(s)