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

--github-repository=<owner/name>

The repository the run belongs to. Default: $GITHUB_REPOSITORY, which a workflow sets.

--github-pipeline-id=<ID>

The workflow run to read - the number in its URL, .../actions/runs/35479251694. Default: $GITHUB_RUN_ID, which a workflow sets.

--trace-file=[<format>:]<file>

Write the trace. Default format: otlp-json, currently the only one.

--gantt=[<format>:]<file>

Draw the run as a Gantt chart: matplotlib-png, matplotlib-svg or matplotlib-pdf.

--force

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.