Skip to content

[bin+brew] ?brew-changelog?: Explore ways to display CHANGELOGs for brew outdated / gh outdated / similar #27

@0xdevalias

Description

@0xdevalias

It would be useful to be able to easily see / review CHANGELOG entries when using brew outdated, or after a brew update / before a brew upgrade, etc.

Prior Art

My Approach

Here is the exploration / approach that I wrote up in the following issue:

As an alternative approach to needing to statically identify the CHANGELOGs for tools up front; I was thinking for a lot of things (particularly those hosted on GitHub), there are common patterns of a CHANGELOG.md file in the repo root, or using GitHub Releases, etc; that could allow us to more dynamically fetch the details (which might also mean a quicker path to adoption in core with less potential Homebrew maintainer burden/etc: Homebrew/brew#3399)

As a starting point, I stumbled on this other tool by @ltaupiac that seems to have a similar sort of goal, focussed purely on GitHub, and written in fish shell scripts:

It basically seems to do the following:

  • Tries brew ruby -e "Formula['name'].head.url" to get the repo URL.
  • Falls back to brew info --json=v2 to extract the homepage if the head URL isn’t available
    • brew info --json=v2 "$formula_name" | jq -r '.formulae[]?.homepage // empty, .casks[]?.homepage // empty'
  • Cleans up .git suffix if present
    • set repo (echo $repo | sed 's#\.git$##')
  • Only continues if the repo host is github.com; otherwise, exits with a message that only GitHub is supported.
    • set -l current_host (trurl --get {host} "$repo")
    • if test "$current_host" != "github.com"
        # ..snip..
      end
      
  • Appends /releases/latest to the repo URL.
    • set repo (echo $repo | sed 's#$#/releases/latest#')
  • Rewrites host to api.github.com.
    • set repo (trurl --set host='api.github.com' $repo)
  • Adjusts the path to prefix with /repos/..., turning it into the correct GitHub API endpoint for the latest release.
    • set -l extracted_path (trurl --get {path} "$repo")
    • set -l path "/repos$extracted_path"
    • set -l repo (trurl --set path="$path" "$repo")
  • Performs a curl request to the GitHub API endpoint, extracts .tag_name and .body from the JSON using jq, pipes that through glow -p to render the changelog nicely in the terminal.

If I was going to approach trying to solve this from scratch today myself, I would probably largely rely on the GitHub CLI gh for the 'heavy lifting', as well as relevant API endpoints:

  • https://cli.github.com/
  • https://github.com/cli/cli
    • GitHub CLI

    • GitHub’s official command line tool

    • gh is GitHub on the command line. It brings pull requests, issues, and other GitHub concepts to the terminal next to where you are already working with git and your code.

  • https://docs.github.com/en/rest/repos/contents#get-repository-content
    • Get repository content

    • Gets the contents of a file or directory in a repository. Specify the file path or directory with the path parameter. If you omit the path parameter, you will receive the contents of the repository's root directory.

    • gh api repos/OWNER/REPO/contents/CHANGELOG.md
    • gh api repos/OWNER/REPO/contents/CHANGELOG.md --jq '{ name, content, encoding, html_url, download_url }'
    • gh api repos/OWNER/REPO/contents/CHANGELOG.md \
        --jq '{
          name,
          html_url,
          download_url,
          encoding,
          content: (if .encoding == "base64" then (.content | @base64d) else .content end)
        }
        | if .encoding == "base64" then del(.encoding) else . end'

If we wanted to render markdown nicely in the terminal ourself, we could use:

And then for releases...

We can list them:

List releases in a repository

For more information about output formatting flags, see `gh help formatting`.

USAGE
  gh release list [flags]

ALIASES
  gh release ls

FLAGS
      --exclude-drafts         Exclude draft releases
      --exclude-pre-releases   Exclude pre-releases
  -q, --jq expression          Filter JSON output using a jq expression
      --json fields            Output JSON with the specified fields
  -L, --limit int              Maximum number of items to fetch (default 30)
  -O, --order string           Order of releases returned: {asc|desc} (default "desc")
  -t, --template string        Format JSON output using a Go template; see "gh help formatting"

INHERITED FLAGS
      --help                     Show help for command
  -R, --repo [HOST/]OWNER/REPO   Select another repository using the [HOST/]OWNER/REPO format

JSON FIELDS
  createdAt, isDraft, isLatest, isPrerelease, name, publishedAt, tagName

..snip..

We can see a scrollable list like this:

gh release list --repo OWNER/REPO

Or we could list releases (up to the limit) as JSON like this:

gh release list --exclude-drafts --exclude-pre-releases --repo OWNER/REPO --json 'createdAt,publishedAt,isLatest,name,tagName'

And filter those to only ones after a specified version like this:

AFTER_TAG="4.6.9"

gh release list \
  --exclude-drafts --exclude-pre-releases \
  --repo Homebrew/brew \
  --json 'createdAt,publishedAt,isLatest,name,tagName' \
  --jq '
    def norm: ltrimstr("v");
    def ver: split(".") | map(tonumber);

    map(select((.tagName // .name | norm | ver) > ("'"$AFTER_TAG"'" | norm | ver)))
  '

And then we can view information about the releases too:

View information about a GitHub Release.

Without an explicit tag name argument, the latest release in the project
is shown.

For more information about output formatting flags, see `gh help formatting`.

USAGE
  gh release view [<tag>] [flags]

FLAGS
  -q, --jq expression     Filter JSON output using a jq expression
      --json fields       Output JSON with the specified fields
  -t, --template string   Format JSON output using a Go template; see "gh help formatting"
  -w, --web               Open the release in the browser

INHERITED FLAGS
      --help                     Show help for command
  -R, --repo [HOST/]OWNER/REPO   Select another repository using the [HOST/]OWNER/REPO format

JSON FIELDS
  apiUrl, assets, author, body, createdAt, databaseId, id, isDraft, isPrerelease,
  name, publishedAt, tagName, tarballUrl, targetCommitish, uploadUrl, url,
  zipballUrl

..snip..

Such as:

  • Latest release in terminal: gh release view --repo OWNER/REPO
  • Latest release on web: gh release view --repo OWNER/REPO --web
  • Specific release in terminal: gh release view --repo OWNER/REPO 1.2.3
  • Specific release on web: gh release view --repo OWNER/REPO 1.2.3 --web
  • We could also get various parts of that as JSON/etc if we wanted, but that seems less relevant here

So then we could also combine that together by using fzf or similar; so that we can browse through the various releases, and preview/open their CHANGELOG in the browser:

OWNER="Homebrew"; \
REPO="brew"; \
AFTER_TAG="4.6.9"; \
\
gh release list \
  --exclude-drafts --exclude-pre-releases \
  --repo "$OWNER/$REPO" \
  --json 'tagName,name,createdAt,publishedAt' \
  --jq 'def norm: ltrimstr("v");
        def ver: split(".") | map(tonumber);

        (["TAG","NAME","CREATED","PUBLISHED"]),
        (map(select((.tagName // .name | norm | ver) > ("'"$AFTER_TAG"'" | norm | ver)))
         | map([.tagName, .name, .createdAt, .publishedAt])[])
        | @tsv' \
| column -ts $'\t' \
| FZF_DEFAULT_COMMAND= \
  fzf \
    --prompt "Releases> " \
    --info=inline-right \
    --input-border \
    --header-lines=1 \
    --footer $'Enter: View in Terminal    Ctrl-O: Open in Browser    Ctrl-P: Toggle Preview' \
    --footer-border=line \
    --reverse \
    --delimiter='[[:space:]]{2,}' \
    --nth '1..' \
    --with-nth '1..' \
    --accept-nth 1 \
    --preview 'GH_FORCE_TTY=1 gh release view --repo '"$OWNER/$REPO"' {1}' \
    --preview-window 'right,60%,border-left,wrap,hidden' \
    --bind 'enter:execute(gh release view --repo '"$OWNER/$REPO"' {1})' \
    --bind 'ctrl-o:execute-silent(gh release view --repo '"$OWNER/$REPO"' {1} --web)+abort' \
    --bind 'ctrl-p:toggle-preview'

And with a bit more effort, we could also make a way that can neatly choose the releases or the CHANGELOG.md depending on which is better.

Originally posted by @0xdevalias in Homebrewery/homebrew-changelog#20

Other Things We Could Improve

This could be a useful addition to my gh outdated alias:

outdated: |
!gh_extensions_outdated() {
if [ "$1" = "-h" ] || [ "$1" = "--help" ] || [ $# -ne 0 ]; then
echo "Usage: gh outdated"
echo "Check for outdated GitHub CLI extensions and the gh CLI tool itself."
echo ""
echo "This command performs the following actions:"
echo " - Lists all installed gh extensions."
echo " - Checks if any installed extensions have updates available."
echo " - Offers to upgrade individual extensions if updates are available."
echo " - Checks if the gh CLI tool is up to date and offers to upgrade it if not."
echo ""
echo "Options:"
echo " -h, --help Show this help message and exit."
return 0
fi
gh extension list
echo
echo "Release notes:"
(gh extension list | awk '{print $3}' | sed 's#^# https://github.com/#' | sed 's#$#/releases#')
echo
echo "Checking for outdated extensions..."
outdated_extensions=$(gh extension upgrade --all --dry-run)
outdated_found=false
while read -r line; do
if [[ $line == *"would have upgraded from"* ]]; then
outdated_found=true
extension_name=$(echo "$line" | awk '{print $1}' | tr -d '[]:')
current_version=$(echo "$line" | awk '{print $(NF-2)}')
new_version=$(echo "$line" | awk '{print $NF}')
echo " Extension $extension_name is outdated (Current: $current_version, Available: $new_version)"
# echo "Do you want to upgrade $extension_name? [y/N]"
# read -r upgrade_extension
# if [ "$upgrade_extension" = "y" ]; then
# gh extension upgrade "$extension_name"
# fi
fi
done <<< "$outdated_extensions"
if [[ $outdated_found == true ]]; then
echo "Do you want to upgrade all outdated extensions? [y/N]"
read -r answer
if [ "$answer" = "y" ]; then
gh extension upgrade --all
fi
else
echo " All extensions are up to date!"
fi
echo
echo "Checking to see if gh is up to date..."
if brew outdated gh | grep -q 'gh'; then
echo "gh is outdated. Do you want to upgrade? [y/N]"
read -r upgrade_gh
if [ "$upgrade_gh" = "y" ]; then
brew upgrade gh
fi
else
echo " gh is up to date!"
fi
}; gh_extensions_outdated $@

Which currently has some crude URL manipulation for showing release notes:

echo "Release notes:"
(gh extension list | awk '{print $3}' | sed 's#^# https://github.com/#' | sed 's#$#/releases#')

See Also

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions