From 1d736df1e4678223c41222e9c11a2663512f06a8 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah Date: Sat, 4 Jul 2026 17:36:48 -0700 Subject: [PATCH] Add generic GitHub CLI Bash helpers --- CHANGELOG.md | 6 ++ README.md | 6 ++ lib/bash/README.md | 2 + lib/bash/gh/README.md | 35 +++++++++++ lib/bash/gh/lib_gh.sh | 55 +++++++++++++++++ lib/bash/gh/tests/lib_gh.bats | 113 ++++++++++++++++++++++++++++++++++ tests/lint-warnings.sh | 1 + tests/validate.sh | 5 ++ 8 files changed, 223 insertions(+) create mode 100644 lib/bash/gh/README.md create mode 100644 lib/bash/gh/lib_gh.sh create mode 100644 lib/bash/gh/tests/lib_gh.bats diff --git a/CHANGELOG.md b/CHANGELOG.md index a3f9df1..6d5d154 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and versions are tracked in the repo-root `VERSION` file. ## [Unreleased] +### Added + +- Added `lib/bash/gh/lib_gh.sh` with generic GitHub CLI availability, + authentication diagnostics, failure reporting, and checked command execution + helpers. + ### Fixed - Hardened `std_run --timeout` retry internals so timeout discovery is cached diff --git a/README.md b/README.md index fb25c47..2e84ecc 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,9 @@ Requires Bash 4.2+. On macOS, use Homebrew Bash instead of the system `/bin/bash - [`lib/bash/git/lib_git.sh`](lib/bash/git/README.md) Git helper functions built on the stdlib for lightweight repository inspection, update, and script freshness checks. +- [`lib/bash/gh/lib_gh.sh`](lib/bash/gh/README.md) + GitHub CLI helper functions built on the stdlib for command readiness, + authentication diagnostics, and checked `gh` execution. - [`lib/bash/str/lib_str.sh`](lib/bash/str/README.md) String helpers built on the stdlib for case conversion, trimming, predicates, splitting, and joining. @@ -83,6 +86,7 @@ Load companion libraries with absolute imports from the same package: ```bash import "$base_bash_libs_prefix/libexec/lib/bash/file/lib_file.sh" import "$base_bash_libs_prefix/libexec/lib/bash/git/lib_git.sh" +import "$base_bash_libs_prefix/libexec/lib/bash/gh/lib_gh.sh" import "$base_bash_libs_prefix/libexec/lib/bash/str/lib_str.sh" import "$base_bash_libs_prefix/libexec/lib/bash/arg/lib_arg.sh" import "$base_bash_libs_prefix/libexec/lib/bash/list/lib_list.sh" @@ -111,6 +115,7 @@ Load companion libraries with absolute imports from the same checkout: ```bash import "$base_bash_libs_dir/lib/bash/file/lib_file.sh" import "$base_bash_libs_dir/lib/bash/git/lib_git.sh" +import "$base_bash_libs_dir/lib/bash/gh/lib_gh.sh" import "$base_bash_libs_dir/lib/bash/str/lib_str.sh" import "$base_bash_libs_dir/lib/bash/arg/lib_arg.sh" import "$base_bash_libs_dir/lib/bash/list/lib_list.sh" @@ -134,6 +139,7 @@ base_bash_libs_dir="$project_root/vendor/base-bash-libs" source "$base_bash_libs_dir/lib/bash/std/lib_std.sh" import "$base_bash_libs_dir/lib/bash/file/lib_file.sh" import "$base_bash_libs_dir/lib/bash/git/lib_git.sh" +import "$base_bash_libs_dir/lib/bash/gh/lib_gh.sh" import "$base_bash_libs_dir/lib/bash/str/lib_str.sh" import "$base_bash_libs_dir/lib/bash/arg/lib_arg.sh" import "$base_bash_libs_dir/lib/bash/list/lib_list.sh" diff --git a/lib/bash/README.md b/lib/bash/README.md index ee23368..24d4c3b 100644 --- a/lib/bash/README.md +++ b/lib/bash/README.md @@ -9,6 +9,8 @@ Reusable Bash libraries for command wrappers and other Bash tooling. shared Bash primitives. - `git/` Git-related helpers built on top of the stdlib. +- `gh/` + GitHub CLI helpers built on top of the stdlib. - `file/` File-editing helpers built on top of the stdlib. - `str/` diff --git a/lib/bash/gh/README.md b/lib/bash/gh/README.md new file mode 100644 index 0000000..681c3a8 --- /dev/null +++ b/lib/bash/gh/README.md @@ -0,0 +1,35 @@ +# GitHub CLI Helpers + +`lib_gh.sh` provides thin wrappers around the GitHub CLI for scripts that want +consistent command checks and authentication diagnostics without adopting Base's +GitHub workflow policy. + +Source the stdlib before this library: + +```bash +source "/path/to/base-bash-libs/lib/bash/std/lib_std.sh" +import "/path/to/base-bash-libs/lib/bash/gh/lib_gh.sh" +``` + +## Public Functions + +- `gh_require_cli [install_hint]` + Verifies that `gh` is available on `PATH`. When it is missing, the helper logs + a generic error and an optional caller-provided install hint. +- `gh_auth_status_diagnostics [login_hint]` + Runs `gh auth status -h github.com`. On failure, it logs non-empty diagnostic + lines from the GitHub CLI and then logs a caller-provided login hint, or the + default `gh auth login -h github.com` hint. +- `gh_report_command_failure [gh args...]` + Logs a failed `gh` command and appends auth diagnostics. The original status + is returned. +- `gh_run [gh args...]` + Runs `gh "$@"` after command availability checks. On command failure, it + reports the failed command and auth diagnostics while preserving the original + exit status. + +## Boundary + +This library is intentionally generic. It does not know about Base branch +names, issue categories, GitHub Project fields, repository baselines, generated +pull request bodies, or any other Base workflow policy. diff --git a/lib/bash/gh/lib_gh.sh b/lib/bash/gh/lib_gh.sh new file mode 100644 index 0000000..dfe8522 --- /dev/null +++ b/lib/bash/gh/lib_gh.sh @@ -0,0 +1,55 @@ +# shellcheck shell=bash +# +# lib_gh.sh - Generic GitHub CLI helpers for Bash scripts. +# + +[[ -n "${__lib_gh_sourced__:-}" ]] && return 0 +if [[ "${BASE_BASH_LIBS_STDLIB_LOADED:-}" != "1" ]]; then + printf '%s\n' "Error: lib_gh.sh requires lib_std.sh to be sourced first." >&2 + return 1 2>/dev/null || exit 1 +fi +readonly __lib_gh_sourced__=1 + +gh_require_cli() { + local install_hint="${1:-}" + + command -v gh >/dev/null 2>&1 || { + log_error "Required command 'gh' was not found on PATH." + [[ -z "$install_hint" ]] || log_error "$install_hint" + return 1 + } +} + +gh_auth_status_diagnostics() { + local login_hint="${1:-Run 'gh auth login -h github.com' and retry.}" + local auth_output line + + gh_require_cli || return 1 + + auth_output="$(gh auth status -h github.com 2>&1)" || { + while IFS= read -r line || [[ -n "$line" ]]; do + [[ -n "$line" ]] && log_error "gh auth status: $line" + done <<<"$auth_output" + [[ -z "$login_hint" ]] || log_error "$login_hint" + return 1 + } +} + +gh_report_command_failure() { + local status="$1" + shift + + log_error "GitHub command failed: gh $*" + gh_auth_status_diagnostics || true + return "$status" +} + +gh_run() { + local status + + gh_require_cli || return 1 + gh "$@" + status=$? + ((status == 0)) && return 0 + gh_report_command_failure "$status" "$@" +} diff --git a/lib/bash/gh/tests/lib_gh.bats b/lib/bash/gh/tests/lib_gh.bats new file mode 100644 index 0000000..50ec004 --- /dev/null +++ b/lib/bash/gh/tests/lib_gh.bats @@ -0,0 +1,113 @@ +#!/usr/bin/env bats + +load ../../tests/test_helper.sh + +setup() { + setup_test_tmpdir + mkdir -p "$TEST_TMPDIR/bin" + PATH="$TEST_TMPDIR/bin:$BASE_TEST_ORIG_PATH" + source "$BASE_BASH_DIR/std/lib_std.sh" + source "$BASE_BASH_DIR/gh/lib_gh.sh" +} + +create_fake_gh() { + local script="$TEST_TMPDIR/bin/gh" + + cat > "$script" + chmod +x "$script" +} + +@test "lib_gh can be sourced more than once" { + source "$BASE_BASH_DIR/gh/lib_gh.sh" + + [ "$(type -t gh_run)" = "function" ] +} + +@test "lib_gh fails clearly when sourced without stdlib" { + bats_run bash -c 'source "$1"; rc=$?; printf "source-rc=%s\n" "$rc"; exit "$rc"' bash "$BASE_BASH_DIR/gh/lib_gh.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"lib_gh.sh requires lib_std.sh to be sourced first"* ]] + [[ "$output" == *"source-rc=1"* ]] + [[ "$output" != *"command not found"* ]] +} + +@test "gh_require_cli succeeds when gh is on PATH" { + create_fake_gh <<'EOF' +#!/usr/bin/env bash +exit 0 +EOF + + capture_command gh_require_cli + + [ "$status" -eq 0 ] + [ "$output" = "" ] +} + +@test "gh_require_cli reports missing gh with caller hint" { + mkdir -p "$TEST_TMPDIR/no-gh-bin" + + bats_run "$BASH" -c ' + source "$1" + source "$2" + PATH="$3" + gh_require_cli "$4" + ' bash "$BASE_BASH_DIR/std/lib_std.sh" "$BASE_BASH_DIR/gh/lib_gh.sh" "$TEST_TMPDIR/no-gh-bin" "Install GitHub CLI and retry." + + [ "$status" -eq 1 ] + [[ "$output" == *"Required command 'gh' was not found on PATH."* ]] + [[ "$output" == *"Install GitHub CLI and retry."* ]] +} + +@test "gh_auth_status_diagnostics reports bounded auth output and hint" { + create_fake_gh <<'EOF' +#!/usr/bin/env bash +if [[ "$1" == "auth" && "$2" == "status" ]]; then + printf 'auth failed\n' >&2 + printf 'run login\n' >&2 + exit 4 +fi +exit 0 +EOF + + capture_command gh_auth_status_diagnostics "Run a custom login command." + + [ "$status" -eq 1 ] + [[ "$output" == *"gh auth status: auth failed"* ]] + [[ "$output" == *"gh auth status: run login"* ]] + [[ "$output" == *"Run a custom login command."* ]] +} + +@test "gh_run passes through successful gh output" { + create_fake_gh <<'EOF' +#!/usr/bin/env bash +printf 'gh args:' +printf ' <%s>' "$@" +printf '\n' +EOF + + capture_command gh_run issue list --repo owner/repo + + [ "$status" -eq 0 ] + [[ "$output" == *"gh args: <--repo> "* ]] +} + +@test "gh_run reports command failure and auth diagnostics" { + create_fake_gh <<'EOF' +#!/usr/bin/env bash +if [[ "$1" == "auth" && "$2" == "status" ]]; then + printf 'not logged in\n' >&2 + exit 1 +fi +printf 'command failed\n' >&2 +exit 7 +EOF + + capture_command gh_run issue create --title Example + + [ "$status" -eq 7 ] + [[ "$output" == *"command failed"* ]] + [[ "$output" == *"GitHub command failed: gh issue create --title Example"* ]] + [[ "$output" == *"gh auth status: not logged in"* ]] + [[ "$output" == *"Run 'gh auth login -h github.com' and retry."* ]] +} diff --git a/tests/lint-warnings.sh b/tests/lint-warnings.sh index 1002434..6536850 100755 --- a/tests/lint-warnings.sh +++ b/tests/lint-warnings.sh @@ -15,6 +15,7 @@ lint_files=( lib/bash/std/lib_std.sh lib/bash/file/lib_file.sh lib/bash/git/lib_git.sh + lib/bash/gh/lib_gh.sh lib/bash/str/lib_str.sh lib/bash/arg/lib_arg.sh lib/bash/list/lib_list.sh diff --git a/tests/validate.sh b/tests/validate.sh index 4c4c640..b3064eb 100755 --- a/tests/validate.sh +++ b/tests/validate.sh @@ -27,6 +27,9 @@ required_files=( lib/bash/file/tests/lib_file.bats lib/bash/git/lib_git.sh lib/bash/git/tests/lib_git.bats + lib/bash/gh/README.md + lib/bash/gh/lib_gh.sh + lib/bash/gh/tests/lib_gh.bats lib/bash/str/README.md lib/bash/str/lib_str.sh lib/bash/str/tests/lib_str.bats @@ -102,6 +105,7 @@ shellcheck --severity=error \ lib/bash/std/lib_std.sh \ lib/bash/file/lib_file.sh \ lib/bash/git/lib_git.sh \ + lib/bash/gh/lib_gh.sh \ lib/bash/str/lib_str.sh \ lib/bash/arg/lib_arg.sh \ lib/bash/list/lib_list.sh \ @@ -113,6 +117,7 @@ bats \ lib/bash/std/tests/lib_std.bats \ lib/bash/file/tests/lib_file.bats \ lib/bash/git/tests/lib_git.bats \ + lib/bash/gh/tests/lib_gh.bats \ lib/bash/str/tests/lib_str.bats \ lib/bash/arg/tests/lib_arg.bats \ lib/bash/list/tests/lib_list.bats