GitHub Releases

GitHub release inspection and artifact verification with gh, jq, awk, and sha256sum.

Why This Matters

Releases are where software leaves the source tree and becomes an artifact you actually execute. That changes the standard.

Do not treat release downloads like casual web browsing. The correct workflow is:

  1. Inspect release metadata.

  2. Filter for the exact platform and architecture you need.

  3. Download the artifact and the published hashes together.

  4. Verify before execution.

This page captures reusable gh patterns for that workflow so you can apply them to any GitHub project, not just Draw.io.

Core Mental Model

There are three distinct stages:

1. Discovery

What versions exist? Which one is latest? Is it stable or a prerelease?

2. Asset Selection

Which files correspond to Linux? Which ones are x86_64, amd64, arm64, deb, rpm, or AppImage?

3. Verification

Did you download the exact file the maintainer published? Did the checksum match locally?

If you skip stage 3, you are just hoping.

Release Discovery

Start by understanding the release stream before you download anything.

List Recent Releases with State

Good first query: tag, date, draft, prerelease, and release name.

gh api repos/{owner}/{repo}/releases \
  --jq '.[:10][] | [.tag_name, .published_at[:10], .draft, .prerelease, .name] | @tsv'

Use this when you want to quickly spot whether a project publishes drafts or prereleases routinely.

List Stable Releases Only

When you want a production-oriented view, filter out drafts and prereleases.

gh api repos/{owner}/{repo}/releases \
  --jq '[.[] | select(.draft == false and .prerelease == false)]
    | .[:10][]
    | [.tag_name, .published_at[:10], .name]
    | @tsv'

This is the command you want when auditing whether a repo has a sane stable release cadence.

Show the Latest Release as Structured Data

You do not always need the entire JSON body. Pull just the fields you care about.

gh api repos/{owner}/{repo}/releases/latest \
  --jq '{tag: .tag_name, published: .published_at, name: .name, assets: [.assets[].name]}'

This is useful for machine-readable notes, shell capture, or feeding later commands.

Check Rate Limit Before Heavy API Work

If you are doing repeated release audits across many repositories, check GitHub API budget first.

gh api rate_limit \
  --jq '{core_remaining: .resources.core.remaining, core_reset: .resources.core.reset, search_remaining: .resources.search.remaining}'

That keeps you from blaming gh when the real issue is API exhaustion.

Paginate Across Large Release Histories

Some projects have long release histories. If you only query the default response window, your audit will be incomplete.

gh api --paginate repos/{owner}/{repo}/releases \
  --jq '.[] | [.tag_name, .published_at[:10], .prerelease] | @tsv'

Use this when you need the full release history rather than the first page only.

Count Stable vs Pre-Release Tags

gh api --paginate repos/{owner}/{repo}/releases \
  --jq '{
    stable: ([.[] | select(.draft == false and .prerelease == false)] | length),
    prerelease: ([.[] | select(.prerelease == true)] | length),
    draft: ([.[] | select(.draft == true)] | length)
  }'

This is useful when evaluating whether a project has a mature release discipline or mostly ships unstable builds.

Asset Inspection and Filtering

Release pages are noisy. Most projects publish artifacts for multiple operating systems and architectures. Your job is to reduce the set down to the exact artifact family you care about.

List Linux Assets for the Latest Release

gh api repos/{owner}/{repo}/releases/latest \
  --jq '.assets[]
    | select(.name | test("AppImage|deb|rpm"))
    | [.name, .size, .download_count, .browser_download_url]
    | @tsv'

This is the first practical narrowing step for Linux users.

List Only x86_64/amd64 Artifacts

gh api repos/{owner}/{repo}/releases/latest \
  --jq '.assets
    | map(select(.name | test("x86_64|amd64")))
    | sort_by(.size)
    | reverse[]
    | [.name, .size, .download_count]
    | @tsv'

This is useful when a project publishes both architecture and packaging variants, and you want to focus only on your host architecture.

Show Asset Size in MiB

Raw byte counts are technically correct but slow to scan. Convert them when doing human inspection.

gh api repos/{owner}/{repo}/releases/latest \
  --jq '.assets[]
    | [.name, ((.size / 1048576) | floor | tostring) + " MiB", .download_count]
    | @tsv'

Never assume the project publishes hashes in a predictable filename. Query for them.

gh api repos/{owner}/{repo}/releases/latest \
  --jq '.assets[]
    | select(.name | test("SHA256|sha256|checksums|hash"))
    | [.name, .browser_download_url]
    | @tsv'

Summarize Asset Types in a Release

This is useful when you are studying how a project ships software.

gh api repos/{owner}/{repo}/releases/latest \
  --jq '.assets
    | map(.name | capture("(?<ext>\\.[^.]+)$").ext // "none")
    | group_by(.)
    | map({type: .[0], count: length})'

If a release suddenly stops publishing a package type, this will make that obvious.

Practical Download Patterns

Once you know what exists, download only the artifact you intend to execute. Do not fetch everything unless you actually need everything.

Resolve the Latest Tag First

This separates discovery from download.

release_tag="$(gh api repos/{owner}/{repo}/releases/latest --jq '.tag_name')"
printf 'latest tag: %s\n' "$release_tag"

This pattern is better than hardcoding a guessed version string.

Download Target Artifact Plus Hashes

This is the baseline safe pattern.

release_tag="$(gh api repos/{owner}/{repo}/releases/latest --jq '.tag_name')"
gh release download "$release_tag" -R {owner}/{repo} \
  -p 'drawio-x86_64-*.AppImage' \
  -p 'Files-SHA256-Hashes.txt' \
  -D ~/.local/share/appimages

The point is not just convenience. The point is that the checksum file lands beside the artifact you are about to verify.

Inspect a Specific Tag Before Downloading

gh release view v29.6.6 -R jgraph/drawio-desktop --json assets \
  --jq '.assets[] | [.name, .size, .downloadCount] | @tsv'

Do this if you already know the candidate tag and want a final sanity check.

Download a Single Exact Asset

gh release download v29.6.6 -R jgraph/drawio-desktop \
  -p 'drawio-x86_64-29.6.6.AppImage' \
  -p 'Files-SHA256-Hashes.txt' \
  -D ~/.local/share/appimages

Use this when reproducibility matters and you want the exact asset spelled out in shell history.

Dry Inspection with Raw URLs

Sometimes you want the download URLs without downloading yet.

gh api repos/{owner}/{repo}/releases/latest \
  --jq '.assets[] | [.name, .browser_download_url] | @tsv'

That is useful for documentation, audits, or comparing how projects publish artifacts.

Verification Patterns

This is the part most people skip. That is sloppy.

Verify All Downloaded Files Referenced by the Hash File

(
  cd ~/.local/share/appimages &&
  sha256sum -c Files-SHA256-Hashes.txt --ignore-missing
)

--ignore-missing matters because release hash files typically contain hashes for many assets you did not download.

Extract the Published Hash for the Exact AppImage

awk '/drawio-x86_64-.*\.AppImage$/ {print}' ~/.local/share/appimages/Files-SHA256-Hashes.txt

Use this when you want to inspect the maintainer-published line directly.

Compare Published and Local Hash Explicitly

file="$(printf '%s\n' ~/.local/share/appimages/drawio-x86_64-*.AppImage)"
published="$(awk '/drawio-x86_64-.*\.AppImage$/ {print $1}' ~/.local/share/appimages/Files-SHA256-Hashes.txt)"
local="$(sha256sum "$file" | awk '{print $1}')"
printf 'published\t%s\nlocal\t%s\n' "$published" "$local"

This is useful when you want a clean two-line proof instead of relying on sha256sum -c output formatting.

Verify a Release Fileset in Any Directory

asset_dir=~/.local/share/appimages
(
  cd "$asset_dir" &&
  sha256sum -c Files-SHA256-Hashes.txt --ignore-missing 2>&1 | sort
)

The sort is optional, but useful when you want stable output for notes or comparisons.

Reusable Audit Patterns

These patterns are for learning from projects, not just installing one tool once.

Generic Repo Variable Pattern

repo='jgraph/drawio-desktop'
gh api "repos/$repo/releases/latest" \
  --jq '.assets[] | [.name, .size, .browser_download_url] | @tsv'

Once you start repeating a repository identifier, put it in a variable. That makes the command reusable and less error-prone.

Stable Tag Audit Across a Project

repo='jgraph/drawio-desktop'
gh api "repos/$repo/releases" \
  --jq '[.[] | select(.draft == false and .prerelease == false)]
    | .[]
    | [.tag_name, .published_at[:10]]
    | @tsv'

Good for asking whether a project ships regularly, sporadically, or never really stabilizes.

Release Cadence by Year

repo='jgraph/drawio-desktop'
gh api --paginate "repos/$repo/releases" \
  --jq '[.[] | select(.draft == false)]
    | map(.published_at[:4])
    | group_by(.)
    | map({year: .[0], count: length})'

This helps answer whether a project is actively maintained or only updated in bursts.

Compare Two Release Tags by Asset Inventory

repo='jgraph/drawio-desktop'
diff \
  <(gh release view v29.6.1 -R "$repo" --json assets --jq '.assets[].name' | sort) \
  <(gh release view v29.6.6 -R "$repo" --json assets --jq '.assets[].name' | sort)

This is a strong audit pattern. It shows whether an asset disappeared, was renamed, or a new packaging target was added.

Diff Release Notes Between Two Tags

repo='jgraph/drawio-desktop'
diff \
  <(gh release view v29.6.1 -R "$repo" --json body --jq '.body' | sed -n '1,80p') \
  <(gh release view v29.6.6 -R "$repo" --json body --jq '.body' | sed -n '1,80p')

This is a useful upgrade-review pattern when you want to compare changelog text without opening a browser.

Rank Assets by Download Count

gh api repos/{owner}/{repo}/releases/latest \
  --jq '.assets
    | sort_by(.download_count)
    | reverse[]
    | [.download_count, .name]
    | @tsv'

This is not a trust signal by itself, but it can help identify the artifact most users actually consume.

Extract Release Notes Only

gh api repos/{owner}/{repo}/releases/latest --jq '.body'

Useful for quick changelog review before deciding whether to update.

Show Top Assets Across the Entire Release History

repo='jgraph/drawio-desktop'
gh api --paginate "repos/$repo/releases" \
  --jq '[.[] | .assets[]? | {name, downloads: .download_count}]
    | sort_by(.downloads)
    | reverse
    | .[:20]'

This is more of a research pattern than an install pattern. It helps identify which packaging formats users actually consume over time.

Draw.io Example Workflow

This is the concrete Linux workflow that motivated the page. It is intentionally explicit.

1. Inspect Recent Releases

gh release list -R jgraph/drawio-desktop -L 5

You already used this. It is the correct first step.

2. Inspect Latest Linux Assets

gh api repos/jgraph/drawio-desktop/releases/latest \
  --jq '.assets[]
    | select(.name | test("AppImage|deb|rpm"))
    | [.name, ((.size / 1048576) | floor | tostring) + " MiB"]
    | @tsv'

That narrows the decision to Linux packages only.

3. Create a Persistent XDG-Compliant Location

mkdir -p ~/.local/share/appimages

Use ~/.local/share/appimages for downloaded GUI AppImages you want to keep. Do not use /tmp for something you intend to retain.

4. Download the Exact AppImage and Hash File

gh release download v29.6.6 -R jgraph/drawio-desktop \
  -p 'drawio-x86_64-29.6.6.AppImage' \
  -p 'Files-SHA256-Hashes.txt' \
  -D ~/.local/share/appimages

This gives you a reproducible history entry showing exactly what version you accepted.

5. Verify Before Execution

(
  cd ~/.local/share/appimages &&
  sha256sum -c Files-SHA256-Hashes.txt --ignore-missing
)

If that does not validate cleanly, stop there. Do not execute the file.

6. Make the AppImage Executable

chmod 755 ~/.local/share/appimages/drawio-x86_64-29.6.6.AppImage

7. Optional Shell Wrapper

ln -sf ~/.local/share/appimages/drawio-x86_64-29.6.6.AppImage ~/.local/bin/drawio

This makes terminal launch simple while keeping the actual binary in a clean XDG data location.

8. Launch the Application

~/.local/share/appimages/drawio-x86_64-29.6.6.AppImage

Or, if you created the symlink:

drawio

Other Real-World Examples

Draw.io is just one case. The workflow applies to CLI tools, editors, and infrastructure binaries too.

Example: Neovim AppImage

Use this when you want the upstream Neovim binary rather than a distro package.

# INSPECT RECENT RELEASES
gh release list -R neovim/neovim -L 5

# SHOW LINUX ASSETS FOR THE LATEST RELEASE
gh api repos/neovim/neovim/releases/latest \
  --jq '.assets[]
    | select(.name | test("appimage|linux"; "i"))
    | [.name, ((.size / 1048576) | floor | tostring) + " MiB"]
    | @tsv'

# DOWNLOAD A KNOWN APPIMAGE + SHA256 FILE
mkdir -p ~/.local/share/appimages
gh release download stable -R neovim/neovim \
  -p 'nvim-linux-x86_64.appimage' \
  -p 'nvim-linux-x86_64.appimage.sha256sum' \
  -D ~/.local/share/appimages

# VERIFY AGAINST THE PUBLISHED HASH FILE
(
  cd ~/.local/share/appimages &&
  sha256sum -c nvim-linux-x86_64.appimage.sha256sum
)

The pattern is the same: inspect, target the correct architecture, download the hash material, verify.

Example: bat Debian Package

Not every project ships an AppImage. Sometimes you want the .deb directly for inspection or offline install.

# LIST AMD64 LINUX PACKAGES
gh api repos/sharkdp/bat/releases/latest \
  --jq '.assets[]
    | select(.name | test("amd64.*deb|x86_64.*deb"))
    | [.name, .browser_download_url]
    | @tsv'

# DOWNLOAD THE .deb PLUS CHECKSUMS
mkdir -p ~/Downloads/releases/bat
gh release download v0.26.0 -R sharkdp/bat \
  -p '*amd64.deb' \
  -p '*SHA256*' \
  -D ~/Downloads/releases/bat

# VERIFY PUBLISHED HASHES BEFORE INSTALL INSPECTION
(
  cd ~/Downloads/releases/bat &&
  sha256sum -c ./*SHA256* --ignore-missing
)

# INSPECT PACKAGE METADATA BEFORE INSTALLING
dpkg-deb -I ~/Downloads/releases/bat/*.deb

That is the right pattern when you want to inspect package metadata first instead of immediately installing.

Example: fd Static Binary Audit

Some projects publish tarballs or standalone binaries rather than distro-native packages.

# LIST LINUX x86_64 ASSETS
gh api repos/sharkdp/fd/releases/latest \
  --jq '.assets[]
    | select(.name | test("x86_64.*linux|amd64.*linux|x86_64.*unknown-linux"))
    | [.name, .size, .download_count]
    | @tsv'

# COMPARE ASSET INVENTORY BETWEEN TWO TAGS
repo='sharkdp/fd'
diff \
  <(gh release view v10.2.0 -R "$repo" --json assets --jq '.assets[].name' | sort) \
  <(gh release view v10.3.0 -R "$repo" --json assets --jq '.assets[].name' | sort)

This is useful when you are maintaining your own install scripts and want to know whether upstream changed naming conventions.

Example: Kubernetes Release Research

This is less about direct download and more about release intelligence.

# SHOW THE LAST 12 STABLE KUBERNETES RELEASE TAGS
gh api --paginate repos/kubernetes/kubernetes/releases \
  --jq '[.[] | select(.draft == false and .prerelease == false)]
    | .[:12][]
    | [.tag_name, .published_at[:10]]
    | @tsv'

# COUNT RELEASES BY YEAR
gh api --paginate repos/kubernetes/kubernetes/releases \
  --jq '[.[] | select(.draft == false)]
    | map(.published_at[:4])
    | group_by(.)
    | map({year: .[0], count: length})'

This is useful when you are studying project maturity, patch cadence, and maintenance behavior.

Example: Your Own Repository Releases

The same patterns apply to your own projects once you start publishing assets.

repo='EvanusModestus/association-engine'

# SEE WHETHER A REPO HAS STARTED SHIPPING RELEASES AT ALL
gh api "repos/$repo/releases" \
  --jq '[.[] | {tag: .tag_name, assets: (.assets | length), published: .published_at}]'

# QUICK VIEW OF THE LATEST RELEASE BODY AND ASSET COUNT
gh api "repos/$repo/releases/latest" \
  --jq '{tag: .tag_name, assets: (.assets | length), body: .body}'

This is useful for self-audit: are you actually packaging software, or just tagging commits?

When to Use gh api vs gh release

Use gh release …​ when you want a higher-level release workflow:

  • list releases

  • inspect a known release

  • download assets

Use gh api …​ when you want precision and filtering:

  • custom field extraction

  • jq transforms

  • architecture or packaging filters

  • audit/reporting workflows

The usual pattern is:

  1. use gh api to inspect and filter

  2. use gh release download to fetch

Anti-Patterns

Avoid these:

  • downloading by guessing a version string before inspecting release metadata

  • executing an AppImage before checksum verification

  • downloading every asset in a release when you only need one file

  • using /tmp for a tool you intend to keep

  • assuming latest always means stable enough for your use case without checking prerelease behavior

Minimal Reusable Template

If you only remember one pattern, remember this one:

repo='owner/repo'
asset_dir=~/.local/share/appimages
mkdir -p "$asset_dir"
tag="$(gh api "repos/$repo/releases/latest" --jq '.tag_name')"
gh api "repos/$repo/releases/latest" --jq '.assets[] | [.name, .browser_download_url] | @tsv'
gh release download "$tag" -R "$repo" -p '*.AppImage' -p '*SHA*' -D "$asset_dir"
( cd "$asset_dir" && sha256sum -c ./*SHA* --ignore-missing )

Refine the asset pattern before using it in production. Wildcards are powerful, but exact filenames are better when reproducibility matters.

Setup Pattern

repo='owner/repo'
asset_dir=~/.local/share/appimages
mkdir -p "$asset_dir"
tag="$(gh api "repos/$repo/releases/latest" --jq '.tag_name')"

Discovery

gh release list -R "$repo" -L 10
gh api "repos/$repo/releases/latest" --jq '{tag: .tag_name, published: .published_at, assets: (.assets | length)}'
gh api --paginate "repos/$repo/releases" --jq '.[] | [.tag_name, .published_at[:10], .draft, .prerelease] | @tsv'
gh api rate_limit --jq '{core_remaining: .resources.core.remaining, search_remaining: .resources.search.remaining}'

Asset Filtering

gh api "repos/$repo/releases/latest" --jq '.assets[] | [.name, .size, .download_count] | @tsv'
gh api "repos/$repo/releases/latest" --jq '.assets[] | select(.name | test("AppImage|deb|rpm|tar.gz|zip")) | [.name, .browser_download_url] | @tsv'
gh api "repos/$repo/releases/latest" --jq '.assets[] | select(.name | test("x86_64|amd64|arm64|aarch64")) | [.name, .size] | @tsv'
gh api "repos/$repo/releases/latest" --jq '.assets[] | select(.name | test("SHA256|sha256|checksums|hash")) | [.name, .browser_download_url] | @tsv'

Safe Download

gh release download "$tag" -R "$repo" -p '*.AppImage' -p '*SHA*' -D "$asset_dir"
gh release download "$tag" -R "$repo" -p '*.deb' -p '*SHA*' -D "$asset_dir"
gh release view "$tag" -R "$repo" --json assets --jq '.assets[] | [.name, .downloadCount] | @tsv'

Verification

( cd "$asset_dir" && sha256sum -c ./*SHA* --ignore-missing )
awk '/AppImage$|\.deb$|\.rpm$|\.tar\.gz$|\.zip$/ {print}' "$asset_dir"/*SHA*
file="$(printf '%s\n' "$asset_dir"/* | grep -Ev 'SHA|sha|checksums|hash')"; sha256sum "$file"

Comparison and Audit

diff <(gh release view v1.0.0 -R "$repo" --json assets --jq '.assets[].name' | sort) <(gh release view v1.1.0 -R "$repo" --json assets --jq '.assets[].name' | sort)
gh api --paginate "repos/$repo/releases" --jq '{stable: ([.[] | select(.draft == false and .prerelease == false)] | length), prerelease: ([.[] | select(.prerelease == true)] | length), draft: ([.[] | select(.draft == true)] | length)}'
gh api --paginate "repos/$repo/releases" --jq '[.[] | select(.draft == false)] | map(.published_at[:4]) | group_by(.) | map({year: .[0], count: length})'

Example Repositories

# GUI AppImage
repo='jgraph/drawio-desktop'

# Editor AppImage
repo='neovim/neovim'

# CLI .deb/.rpm
repo='sharkdp/bat'

# Binary asset research
repo='sharkdp/fd'

# Large project release cadence
repo='kubernetes/kubernetes'

See Also

  • Tags — version markers that often back releases

  • Remotes — pushing tags and release-oriented workflows

  • curl — raw API inspection when gh is unavailable