pytooling-github
pyTooling.GitHub installs a program: pytooling-github. It is the command line front-end to what the package models - reading a GitHub Actions pipeline run into a trace, writing that trace, rendering it - so a pipeline job can do those things without a script of its own.
pytooling-github help # what the program can do
pytooling-github help <command> # what one command can do
pytooling-github version # which pyTooling.GitHub is installed
The program is the console_scripts entry point pyTooling.GitHub.CLI:main, which setup.py
registers, so it is on the path after pip install pyTooling.GitHub.
Attention
Drawing needs the diagram extra: pip install pyTooling.GitHub[diagram] installs
matplotlib, which --gantt draws with. Without it, --gantt names the missing package and the
command line installing it rather than failing on an import. colorama, which a
TerminalApplication needs to write anything at all, comes with the package’s
requirement pyTooling[terminal].
How a command is declared
The program is a TerminalApplication and an
ArgParseHelperMixin, so it prints like the first and parses like the
second: a command is a method marked with CommandHandler, and the
arguments of that command are the attributes written above the method. See ArgParse for the attributes
themselves.
@CommandHandler("version", help="Display version information.")
def HandleVersion(self, _: Namespace) -> None:
...
Two commands are always there: help, which prints the help page of the program or of one command, and
version. A call with no command prints the help page.
A group of related commands is a mixin-class of its own.
Application inherits from all of them, and the attributes are found on the assembled
class, so adding a command means writing a mixin and adding one base-class - see Split Handlers into multiple classes. That is the
same construction pyedaa-outputfilter uses.
Hint
The three global switches - -q / --quiet, -v / --verbose and -d / --debug - are declared
on the default handler, so they belong before the command: pytooling-github --verbose version, not
pytooling-github version --verbose.
What a failure looks like
main() runs the program inside a try ... except, so a user of the program sees a message
and a non-zero exit code rather than a traceback. A ToolingException is printed with
its cause and with every note it carries, because the notes are where pyTooling puts the advice - “check the
repository’s name” rather than only “404”.
A MissingDependencyError is not handled where it is raised. An output needing an
optional package - --gantt needs matplotlib - imports it where it draws and lets the exception travel to
main(), which hands it to
PrintMissingDependencyError(): that printer names the missing
package and every command line installing it, and reports no bug, because nothing is wrong with the program.
The pipeline command
pytooling-github pipeline reads one run of a CI pipeline into a Trace and writes
what was asked of it. Reading and writing are separate steps: the trace is the intermediate every output is
derived from, so a further output is a further option rather than a second reader.
pytooling-github pipeline --github-repository=pyTooling/pyTooling \
--github-pipeline-id=35479251694 \
--trace-file=report/Pipeline.otlp.json \
--gantt=report/Pipeline.png
Option |
Meaning |
|---|---|
|
The repository the run belongs to. Default: |
|
The workflow run to read - the number in its URL, |
|
Write the trace. Default format: |
|
Draw the run as a Gantt chart: |
|
Overwrite files that exist. Without it, an existing file is an error. |
The token is read from $GITHUB_TOKEN. Inside a workflow, the built-in token with the
actions: read permission suffices; outside one, it raises the rate limit and is required for a private
repository.
Hint
A job cannot see itself. It is still running when it reads the run, so a job that reports the pipeline’s timing depends on every other job and runs last.
[<format>:]<file>
Every option that writes a file takes the format in front of the path, separated by a colon, and assumes a
default when none is given - --trace-file=report/Pipeline.otlp.json and
--trace-file=otlp-json:report/Pipeline.otlp.json are the same thing.
A colon alone doesn’t make a format. A format is more than one character long and holds no path separator, so
a Windows drive (C:\report\trace.json) and a colon deeper down a path are paths. A prefix that looks
like a format but isn’t one is an error naming the formats that exist, rather than a file with a strange name.
None of that is the command’s own: splitFormat() does the splitting for any
program declaring an option of this shape, and a value naming no format gets the enumeration’s DEFAULT - see
pyTooling’s argument formats.
Both are checked before the pipeline is read, so a misspelled format or a file that exists is reported at once instead of after a network round-trip.
Drawing the run
--gantt lays the trace out as a Gantt chart
and draws it: one row per job, the time it waited for a runner in light gray in front of the time it ran, and a line for
the pipeline and every called workflow. The legend carries the statistics per runner image. The steps are left out -
a pipeline of 57 jobs has more than a thousand of them, and a chart of one row per step is a different picture.
A format is the backend and the file format - matplotlib-png, matplotlib-svg, matplotlib-pdf - and the
file’s suffix has to agree with it. A value naming no format gets matplotlib-png, so a PNG needs no format, and
any other file states it: --gantt=matplotlib-svg:report/Pipeline.svg. A suffix that disagrees with the
format is reported rather than a file being written under a name that lies about its content.
matplotlib is an optional dependency. Without it, --gantt reports which extra installs it
(pyTooling[diagram]) instead of failing on an import.
Hint
In an SVG, every bar or line is a group named after the timespan it draws - span-<SpanID> - so a script can
find the elements of a job.
pyTooling’s rendering section describes
the identifiers.