Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions lib/bash/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/`
Expand Down
35 changes: 35 additions & 0 deletions lib/bash/gh/README.md
Original file line number Diff line number Diff line change
@@ -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 <status> [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.
55 changes: 55 additions & 0 deletions lib/bash/gh/lib_gh.sh
Original file line number Diff line number Diff line change
@@ -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" "$@"
}
113 changes: 113 additions & 0 deletions lib/bash/gh/tests/lib_gh.bats
Original file line number Diff line number Diff line change
@@ -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: <issue> <list> <--repo> <owner/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."* ]]
}
1 change: 1 addition & 0 deletions tests/lint-warnings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions tests/validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 \
Expand All @@ -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
Expand Down
Loading