Releases and Tags
pyTooling.GitHub.Release models the releases and tags of a GitHub repository, and
ReleaseReader reads them through GitHub’s GraphQL API:
from os import getenv
from pyTooling.GitHub.Release import SourceArchive
from pyTooling.GitHub.ReleaseReader import ReleaseReader
repository = ReleaseReader("ghdl", "ghdl", token=getenv("GITHUB_TOKEN")).Read()
release = repository["v5.0.1"] # the release published at tag 'v5.0.1'
print(f"{release.Name or release.TagName}: {release.PublishedAt:%d.%m.%Y %H:%M:%S} {release.CommitSHA[:7]}")
for asset in release: # the files attached to the release
print(f" {asset.Label or asset.Name}: {asset.Size} bytes, {asset.DownloadURL}")
print(release.SourceArchiveURL(SourceArchive.Tarball))
for tag in repository.Tags.values(): # every tag, with or without a release
if not tag.HasRelease:
print(f"{tag.TagName} at {tag.CommitSHA[:7]}, committed {tag.CommittedAt}")
The Tree
▾▸
Repositorythe releases and tags of a repositoryTaga tag and the commit it points to
Every element knows its Parent. A repository is a collection of its releases: "v5.0.1" in repository asks
whether a release is published at that tag, repository["v5.0.1"] returns it, and iterating yields the releases
the newest first. Its Tags hold every tag, and
Tag.HasRelease tells the tags without a release - OSVVM, for
example, tags far more versions than it publishes releases of. A release is a collection of its assets in the same
way, by file name.
A release and a tag share their base-class CommitReference: the tag’s name
(TagName) and the commit it points to (CommitSHA, CommitURL).
A release’s commit is the commit its tag points to. GitHub’s
target_commitishis usually a branch’s name, e.g.main, and no commit. An annotated tag is resolved through its tag object.Drafts are skipped. A draft is unpublished and may change, so it is no part of a repository’s releases - even for a token with write access, which GitHub reports drafts to.
A release’s time is when it was published (
PublishedAt). The time GitHub calls created is the commit’s.Every time keeps its time zone - GitHub reports UTC -, so a consumer converts it to the time zone it shows; a constructor rejects a time without one.
A release without a title has
NameNone; GitHub’s release page shows the tag’s name instead. The release notes are GitHub Flavored Markdown.IsLatestis GitHub’s latest release, which a maintainer may also set by hand;LatestReleasereturns it.An asset’s
Labelis the text GitHub’s release page shows instead of the file name, if the uploader set one. ItsDigestis the checksum GitHub computed on upload, e.g.sha256:8c7dd2e9...; an asset uploaded before GitHub computed checksums has none.GitHub’s Source code archives are no assets: GitHub creates them on request.
SourceArchiveURL()returns the URL the release page links.
Note
Download counts are not part of the model: they change with every download, so a stored snapshot would never be the same twice.
Reading a Repository
GitHub’s GraphQL API serves no anonymous requests, so ReleaseReader needs a
token - inside a workflow, GITHUB_TOKEN with the contents: read permission suffices.
One query (
RELEASES_QUERY) reads a page of 50 releases with their assets and a page of 100 tags with their commits. Both are read page by page, until each is complete.GitHub’s GraphQL API reports no asset labels. Each release with assets costs one request to the REST API, which does.
A request is tried again after a transient failure. A failed request, and an error GitHub’s GraphQL API responds with, raise a
GitHubError, whose notes report the cause - e.g. an unknown repository, an exhausted rate limit or an invalid token.
Snapshots
ToSnapshot() writes the model as a JSON object, which
ParseSnapshot() reads back - so a consumer stores a repository’s
releases and reads them again without another request. The snapshot holds a format version
(SnapshotVersion), the time the data was read
(FetchedAt), and the repository in the shape of GitHub’s GraphQL API -
all pages merged, drafts skipped and each asset’s label added:
{
"version": "1",
"fetchedAt": "2026-10-15T21:05:44+00:00",
"repository": {
"nameWithOwner": "ghdl/ghdl",
"url": "https://github.com/ghdl/ghdl",
"releases": {"nodes": [
{
"tagName": "v6.0.0", "name": null, "description": "# GHDL 6.0.0 ...",
"publishedAt": "2026-03-07T18:38:42+00:00", "url": "https://github.com/ghdl/ghdl/releases/tag/v6.0.0",
"isDraft": false, "isPrerelease": false, "isLatest": true,
"tagCommit": {"oid": "e589c698...", "url": "https://github.com/ghdl/ghdl/commit/e589c698..."},
"releaseAssets": {"nodes": [
{
"name": "ghdl-llvm-6.0.0-macos14-aarch64.tar.gz",
"label": "GHDL - v6.0.0 - macOS 14 (aarch64) - llvm backend",
"contentType": "application/x-gtar", "size": 42493570,
"downloadUrl": "https://github.com/ghdl/ghdl/releases/download/v6.0.0/ghdl-llvm-6.0.0-...",
"digest": "sha256:..."
}
]}
}
]},
"refs": {"nodes": [
{"name": "v6.0.0", "target": {"__typename": "Tag", "target": {
"__typename": "Commit", "oid": "e589c698...", "url": "https://github.com/ghdl/ghdl/commit/e589c698...",
"committedDate": "2026-03-07T17:47:40+00:00"
}}}
]}
}
}
Repository.Parse reads both, GitHub’s response and the
snapshot’s repository. A connection still announcing a further page is rejected instead of read as complete.