Skip to main content

Retrieve the latest stable tag from a GitHub repository using the GitHub API. This guide shows you how to fetch tags and filter for stable releases in both Bash and PowerShell.

---
title: Get Latest Stable GitHub Tag
subtitle: 'Retrieve the latest stable tag from a GitHub repository using the [GitHub API](https://docs.github.com/en/rest/git/tags). This guide shows you how to fetch tags and filter for stable releases in both Bash and PowerShell.'
author: Jon LaBelle
date: February 2, 2026
source: https://jonlabelle.com/snippets/view/markdown/get-latest-stable-github-tag
gist: https://gist.github.com/jonlabelle/2e091fa88baf70c3b8d6aa36d43c4ee8
---

Retrieve the latest stable tag from a GitHub repository using the [GitHub API](https://docs.github.com/en/rest/git/tags). This guide shows you how to fetch tags and filter for stable releases in both Bash and PowerShell.

> [!Note]
> This strategy assumes the repository tags follow the [Semantic Versioning](https://semver.org) format.

## Table of Contents

- [The Challenge](#the-challenge)
- [The Solution](#the-solution)
  - [Understanding the API](#understanding-the-api)
- [Bash Implementation](#bash-implementation)
  - [Basic Command](#basic-command)
  - [macOS Alternative (Without GNU Coreutils)](#macos-alternative-without-gnu-coreutils)
  - [Complete Example with Error Handling](#complete-example-with-error-handling)
  - [With Authentication](#with-authentication)
- [PowerShell Implementation](#powershell-implementation)
  - [Basic Command](#basic-command-1)
  - [Complete Example with Error Handling](#complete-example-with-error-handling-1)
  - [With Authentication](#with-authentication-1)
- [Summary](#summary)

## The Challenge

The GitHub API's `/repos/OWNER/REPO/tags` endpoint returns all tags, including pre-releases (alpha, beta, rc, etc.). Additionally, the API doesn't guarantee tags are returned in any particular order---neither chronological nor semantic. This means we need to:

1. Filter for stable version tags (using a whitelist approach)
2. Sort them semantically to find the true latest version
3. Handle edge cases like pagination and rate limits

## The Solution

We'll use a whitelist regex pattern that matches only clean semantic version tags like `1.2.3` or `v1.2.3`, automatically excluding anything with extensions (e.g., `1.2.3-beta`, `v2.0.0-rc1`).

### Understanding the API

The GitHub API returns 30 tags by default. To increase our chances of finding the latest stable release, we'll request 100 tags using `?per_page=100`. For repositories with many tags, you may need to implement pagination.

GitHub rate limits unauthenticated requests to 60 per hour. Authenticated requests (using a GitHub token) increase this to 5,000 per hour.

## Bash Implementation

### Basic Command

```bash
curl --silent "https://api.github.com/repos/OWNER/REPO/tags?per_page=100" |
  sed -n 's/.*"name": "\([^"]*\)".*/\1/p' |
  grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' |
  sort -V |
  tail -n 1
```

**How it works:**

- `curl --silent` fetches the JSON response without progress output
- `sed` extracts tag names from the JSON
- `grep -E` filters for clean semantic versions (whitelist approach)
- `sort -V` performs version-aware sorting
- `tail -n 1` selects the highest version

> [!Important]
> On macOS, `sort -V` may not be available in the default BSD utilities. Install GNU coreutils (`brew install coreutils`) and use `gsort -V` instead.

### macOS Alternative (Without GNU Coreutils)

If you don't want to install additional tools, use this version that sorts with `awk`:

```bash
OWNER="GoogleCloudPlatform"
REPO="cloud-sql-proxy"

latest_tag=$(curl --silent --fail "https://api.github.com/repos/$OWNER/$REPO/tags?per_page=100" |
  sed -n 's/.*"name": "\([^"]*\)".*/\1/p' |
  grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' |
  awk -F'[.v]' '{
    v = ($1 == "" ? $2 : $1);
    print v*1000000 + $(NF-1)*1000 + $NF "\t" $0
  }' |
  sort -n |
  tail -n 1 |
  cut -f2)

if [ -z "$latest_tag" ]; then
  echo "Error: No stable tag found or API request failed"
  exit 1
fi

echo "Latest stable tag: $latest_tag"
```

This approach converts versions to numeric values for sorting (e.g., `1.2.3` becomes `1002003`), then extracts the original tag name.

### Complete Example with Error Handling

```bash
OWNER="GoogleCloudPlatform"
REPO="cloud-sql-proxy"

latest_tag=$(curl --silent --fail "https://api.github.com/repos/$OWNER/$REPO/tags?per_page=100" |
  sed -n 's/.*"name": "\([^"]*\)".*/\1/p' |
  grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' |
  sort -V |
  tail -n 1)

if [ -z "$latest_tag" ]; then
  echo "Error: No stable tag found or API request failed"
  exit 1
fi

echo "Latest stable tag: $latest_tag"
```

### With Authentication

For higher rate limits, authenticate with a GitHub token:

```bash
OWNER="GoogleCloudPlatform"
REPO="cloud-sql-proxy"
GITHUB_TOKEN="your_token_here"

latest_tag=$(curl --silent --fail \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  "https://api.github.com/repos/$OWNER/$REPO/tags?per_page=100" |
  sed -n 's/.*"name": "\([^"]*\)".*/\1/p' |
  grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' |
  sort -V |
  tail -n 1)

if [ -z "$latest_tag" ]; then
  echo "Error: No stable tag found or API request failed"
  exit 1
fi

echo "Latest stable tag: $latest_tag"
```

## PowerShell Implementation

PowerShell provides native JSON parsing and type-aware sorting, making the implementation more concise.

### Basic Command

```powershell
$tags = Invoke-RestMethod -Uri "https://api.github.com/repos/OWNER/REPO/tags?per_page=100"
$tags.name |
  Where-Object { $_ -match '^v?\d+\.\d+\.\d+$' } |
  Sort-Object { [version]($_ -replace '^v','') } |
  Select-Object -Last 1
```

**How it works:**

- `Invoke-RestMethod` automatically parses the JSON response
- `Where-Object` filters for clean semantic versions
- `Sort-Object` with `[version]` type casting performs semantic version sorting
- `Select-Object -Last 1` selects the highest version

### Complete Example with Error Handling

```powershell
$owner = "GoogleCloudPlatform"
$repo = "cloud-sql-proxy"

try {
    $tags = Invoke-RestMethod -Uri "https://api.github.com/repos/$owner/$repo/tags?per_page=100"
    $latestTag = $tags.name |
      Where-Object { $_ -match '^v?\d+\.\d+\.\d+$' } |
      Sort-Object { [version]($_ -replace '^v','') } |
      Select-Object -Last 1

    if (-not $latestTag) {
        Write-Error "No stable tag found"
        exit 1
    }

    Write-Output "Latest stable tag: $latestTag"
}
catch {
    Write-Error "Failed to fetch tags: $_"
    exit 1
}
```

### With Authentication

```powershell
$owner = "GoogleCloudPlatform"
$repo = "cloud-sql-proxy"
$token = "your_token_here"

$headers = @{
    Authorization = "Bearer $token"
}

try {
    $tags = Invoke-RestMethod -Uri "https://api.github.com/repos/$owner/$repo/tags?per_page=100" -Headers $headers
    $latestTag = $tags.name |
      Where-Object { $_ -match '^v?\d+\.\d+\.\d+$' } |
      Sort-Object { [version]($_ -replace '^v','') } |
      Select-Object -Last 1

    if (-not $latestTag) {
        Write-Error "No stable tag found"
        exit 1
    }

    Write-Output "Latest stable tag: $latestTag"
}
catch {
    Write-Error "Failed to fetch tags: $_"
    exit 1
}
```

## Summary

Both approaches use a whitelist strategy to match only clean semantic version tags, automatically filtering out pre-releases. By sorting the results semantically, we ensure we get the true latest stable version regardless of the API's return order.