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
7 changes: 6 additions & 1 deletion cli/bash/commands/basectl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ such command directories exist. Optional utility CLIs such as `caff` and
provided, project manifest artifacts with suggested fixes.
- `basectl doctor explain <finding-id>` prints local, deterministic guidance
for selected stable finding IDs, with optional JSON output.
- `basectl gh` manages GitHub issues, pull requests, branch naming, repository
- `basectl gh` manages GitHub authentication, issues, pull requests, branch naming, repository
hygiene, and GitHub Project metadata using Base's opinionated workflow. It
uses standard GitHub-style issue categories such as `bug`, `enhancement`,
`documentation`, `ci`, and `security`, and derives branch names from those
Expand All @@ -111,6 +111,11 @@ such command directories exist. Optional utility CLIs such as `caff` and
repository from `--repo`/`-R`, then `GH_REPO`, then `origin`. Issue creation
is unassigned by default unless `--assignee` is passed or
`.github/base-project.yml` sets `project.issue_defaults.assignee`.
`basectl gh auth status` reports the active credential state without printing
token values. `basectl gh auth refresh --scope project` explicitly refreshes
stored credentials and requests the Project scope when needed. Refresh does
not modify `GH_TOKEN`, `GITHUB_TOKEN`, or their Enterprise variants; when one
is set, it takes precedence and must be unset or rotated at its source.
Prefer this command for Base repository GitHub workflows when it supports the
task.
- `basectl onboard` guides first-run setup around existing setup, check,
Expand Down
223 changes: 222 additions & 1 deletion cli/bash/commands/basectl/subcommands/gh.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ source "$BASE_HOME/cli/bash/commands/basectl/subcommands/gh_branch_worktree.sh"
base_gh_usage() {
cat <<'EOF'
Usage:
basectl gh auth status [--hostname <host>]
basectl gh auth refresh [--hostname <host>] [--scope <scope>]... [--scopes <scope,...>] [--clipboard]
basectl gh issue list [gh options...]
basectl gh issue create [--category <bug|enhancement|documentation|ci|security>] --title <title> [--body <body>] [--repo <owner/name>] [--assignee <login>|--no-assignee] [--size <T|S|M|L>] [project options...]
basectl gh issue readiness <number> [--repo <owner/name>] [--project-owner <login> --project-number <number>] [--format <text|json>]
Expand Down Expand Up @@ -54,6 +56,9 @@ Issue categories:

Notes:
- This command requires the GitHub CLI (`gh`) for GitHub operations.
- `auth status` reports credential state without displaying token values.
- `auth refresh` updates stored credentials only. Environment tokens such as
`GH_TOKEN` take precedence and must be rotated or unset separately.
- Issues are unassigned unless --assignee is passed or .github/base-project.yml
sets project.issue_defaults.assignee.
- When the GitHub repo is known, issue create also adds the issue to the
Expand All @@ -69,6 +74,207 @@ Notes:
EOF
}

base_gh_auth_usage() {
cat <<'EOF'
Usage:
basectl gh auth status [--hostname <host>]
basectl gh auth refresh [--hostname <host>] [--scope <scope>]... [--scopes <scope,...>] [--clipboard]

Purpose:
Inspect GitHub authentication safely or refresh the stored credential for an
active account.

Options:
--hostname <host> GitHub host. Defaults to github.com.
--scope <scope> Additional OAuth scope; repeat this option as needed.
--scopes <scope,...> Comma-separated additional OAuth scopes.
--clipboard Copy the one-time OAuth device code to the clipboard.
-h, --help Show this help text.

Notes:
- Refresh is explicit and may open the GitHub browser/device authorization flow.
- Refresh changes stored credentials only. GH_TOKEN/GITHUB_TOKEN (or their
Enterprise variants) override stored credentials for the active process.
- If the stored credential is invalid, run `gh auth login -h <host>` first.
EOF
}

base_gh_auth_environment_token_name() {
local hostname="${1:-github.com}"

case "$hostname" in
github.com|*.ghe.com)
if [[ -n "${GH_TOKEN:-}" ]]; then
printf '%s\n' GH_TOKEN
return 0
fi
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
printf '%s\n' GITHUB_TOKEN
return 0
fi
;;
*)
if [[ -n "${GH_ENTERPRISE_TOKEN:-}" ]]; then
printf '%s\n' GH_ENTERPRISE_TOKEN
return 0
fi
if [[ -n "${GITHUB_ENTERPRISE_TOKEN:-}" ]]; then
printf '%s\n' GITHUB_ENTERPRISE_TOKEN
return 0
fi
;;
esac

return 1
}

base_gh_auth_environment_warning() {
local hostname="${1:-github.com}"
local token_name

token_name="$(base_gh_auth_environment_token_name "$hostname")" || return 1
log_warn "GitHub CLI is using $token_name from the environment. Stored credentials and 'basectl gh auth refresh' will not affect commands until this variable is unset or rotated at its source."
return 0
}

base_gh_auth_status() {
local hostname="${1:-github.com}"
local output
local status=0

base_gh_require_command gh || return 1
base_gh_auth_environment_warning "$hostname" || true
output="$(gh auth status --hostname "$hostname" 2>&1)" || status=$?
[[ -z "$output" ]] || printf '%s\n' "$output"
((status == 0)) && return 0

if grep -Eqi 'lookup .*api\.[^[:space:]]+|error connecting|no such host|network' <<<"$output"; then
log_warn "Unable to reach GitHub while checking authentication. Verify network or DNS and retry."
elif grep -Eqi 'not logged in|invalid|failed to log in|bad credentials|401' <<<"$output"; then
log_warn "GitHub authentication is unavailable for '$hostname'. Run 'gh auth login -h $hostname'."
fi

return "$status"
}

base_gh_auth_status_command() {
local hostname="github.com"

while (($#)); do
case "$1" in
--hostname)
[[ -n "${2:-}" ]] || {
base_gh_usage_error base_gh_auth_usage "Option '$1' requires a host argument."
return $?
}
hostname="$2"
shift
;;
-h|--help)
base_gh_auth_usage
return 0
;;
*)
base_gh_usage_error base_gh_auth_usage "Unknown auth status option '$1'."
return $?
;;
esac
shift
done

base_gh_auth_status "$hostname"
}

base_gh_auth_refresh_command() {
local hostname="github.com"
local clipboard=0
local scope
local scopes_csv=""
local token_name
local args
local status=0

while (($#)); do
case "$1" in
--hostname)
[[ -n "${2:-}" ]] || {
base_gh_usage_error base_gh_auth_usage "Option '$1' requires a host argument."
return $?
}
hostname="$2"
shift
;;
--scope)
[[ -n "${2:-}" ]] || {
base_gh_usage_error base_gh_auth_usage "Option '--scope' requires a scope argument."
return $?
}
scope="$2"
[[ -n "$scopes_csv" ]] && scopes_csv+=,
scopes_csv+="$scope"
shift
;;
--scopes)
[[ -n "${2:-}" ]] || {
base_gh_usage_error base_gh_auth_usage "Option '--scopes' requires a scope argument."
return $?
}
[[ -n "$scopes_csv" ]] && scopes_csv+=,
scopes_csv+="$2"
shift
;;
--clipboard)
clipboard=1
;;
-h|--help)
base_gh_auth_usage
return 0
;;
*)
base_gh_usage_error base_gh_auth_usage "Unknown auth refresh option '$1'."
return $?
;;
esac
shift
done

token_name="$(base_gh_auth_environment_token_name "$hostname")" || true
if [[ -n "$token_name" ]]; then
base_gh_error "Cannot refresh the stored GitHub credential while $token_name is set."
log_warn "Unset $token_name for this process or rotate the environment token at its source, then retry."
return 2
fi

base_gh_require_command gh || return 1
args=(auth refresh --hostname "$hostname")
[[ -z "$scopes_csv" ]] || args+=(--scopes "$scopes_csv")
((clipboard)) && args+=(--clipboard)
base_gh_run "${args[@]}" || status=$?
((status == 0)) || return "$status"
printf "GitHub credentials refreshed for '%s'.\n" "$hostname"
}

base_gh_do_auth() {
local command="${1:-}"
shift || true

case "$command" in
status)
base_gh_auth_status_command "$@"
;;
refresh)
base_gh_auth_refresh_command "$@"
;;
-h|--help|help|"")
base_gh_auth_usage
;;
*)
base_gh_usage_error base_gh_auth_usage "Unknown gh auth command '$command'."
return $?
;;
esac
}

base_gh_issue_usage() {
cat <<'EOF'
Usage:
Expand Down Expand Up @@ -443,7 +649,18 @@ base_gh_report_command_failure() {
}

base_gh_run() {
gh_run "$@"
local status=0

gh_run "$@" || status=$?
((status == 0)) && return 0

base_gh_auth_environment_warning github.com || true
if [[ "${1:-}" == project ]]; then
log_warn "GitHub Project operations require the 'project' scope for stored OAuth credentials."
log_warn "Run 'basectl gh auth refresh --scope project' if the stored credential is active."
fi

return "$status"
}

base_gh_args_request_help() {
Expand Down Expand Up @@ -1349,6 +1566,7 @@ base_gh_issue_create() {
printf '%s\n' "$issue_output"

if ((configure_project)) && [[ -n "$github_repo" ]]; then
base_gh_auth_environment_warning github.com || true
issue_number="$(base_gh_issue_number_from_output "$issue_output")" || {
base_gh_error "Unable to determine created issue number from gh output."
return 1
Expand Down Expand Up @@ -1611,6 +1829,8 @@ base_gh_do_project() {
return 0
fi

base_gh_auth_environment_warning github.com || true

[[ -x "$wrapper" ]] || {
base_gh_error "Base Python wrapper '$wrapper' is missing or is not executable."
return 1
Expand All @@ -1623,6 +1843,7 @@ base_gh_subcommand_main() {
shift || true

case "$area" in
auth) base_gh_do_auth "$@" ;;
issue) base_gh_do_issue "$@" ;;
pr) base_gh_do_pr "$@" ;;
project) base_gh_do_project "$@" ;;
Expand Down
12 changes: 11 additions & 1 deletion cli/bash/commands/basectl/tests/completions.bats
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,14 @@ EOF
COMP_CWORD=2; \
_base_basectl_completion; \
printf "gh_areas=%s\n" "${COMPREPLY[*]}"; \
COMP_WORDS=(basectl gh auth ""); \
COMP_CWORD=3; \
_base_basectl_completion; \
printf "gh_auth_commands=%s\n" "${COMPREPLY[*]}"; \
COMP_WORDS=(basectl gh auth refresh --); \
COMP_CWORD=4; \
_base_basectl_completion; \
printf "gh_auth_refresh_options=%s\n" "${COMPREPLY[*]}"; \
COMP_WORDS=(basectl gh project ""); \
COMP_CWORD=3; \
_base_basectl_completion; \
Expand Down Expand Up @@ -351,7 +359,9 @@ EOF
[[ "$output" == *"ci_check_options=--ci --profile --format --manifest --remote-network"* ]]
[[ "$output" == *"ci_doctor_projects=base demo"* ]]
[[ "$output" == *"ci_doctor_options=--ci --profile --format --manifest --remote-network --no-color"* ]]
[[ "$output" == *"gh_areas=issue pr branch worktree project"* ]]
[[ "$output" == *"gh_areas=auth issue pr branch worktree project"* ]]
[[ "$output" == *"gh_auth_commands=status refresh"* ]]
[[ "$output" == *"gh_auth_refresh_options=--hostname --scope --scopes --clipboard --help"* ]]
[[ "$output" == *"gh_project_commands=doctor configure issue"* ]]
[[ "$output" == *"gh_project_configure_options=--project --owner --schema --config --copy-fields-from --initiative-option --repo --replace-project --dry-run"* ]]
[[ "$output" == *"gh_project_issue_set_fields_options=--repo --project --owner --config --status --priority --area --initiative --size --dry-run"* ]]
Expand Down
Loading
Loading