From c63f11a7aefbe445d03836174f20cf46a59b83a0 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Fri, 8 May 2026 11:58:07 +0200 Subject: [PATCH] Clarify sandbox GitHub auth failures gh auth status can report an invalid hosts.yml token when the real problem is blocked GitHub API connectivity in Codex or another sandbox. The release command and review bot now probe gh api user before asking for re-auth, continue when that probe succeeds, and report network/sandbox failure when the API is unreachable. Constraint: Old gh auth status output can conflate API connectivity failure with invalid stored credentials Rejected: Always tell users to run gh auth login | repeats a false fix when the token is valid but api.github.com is unreachable Confidence: high Scope-risk: narrow Directive: Do not collapse GitHub auth failures back to one re-auth message without preserving API-connectivity diagnostics Tested: bash -n scripts/review-bot-watch.sh Tested: bash -n templates/scripts/review-bot-watch.sh Tested: node --test test/release.test.js Tested: node --test test/agents.test.js Co-authored-by: OmX --- scripts/review-bot-watch.sh | 33 +++++++++- src/cli/main.js | 28 +++++++-- templates/scripts/review-bot-watch.sh | 33 +++++++++- test/agents.test.js | 30 +++++++++ test/release.test.js | 90 +++++++++++++++++++++++++++ 5 files changed, 205 insertions(+), 9 deletions(-) diff --git a/scripts/review-bot-watch.sh b/scripts/review-bot-watch.sh index 064a71a7..c75deb45 100755 --- a/scripts/review-bot-watch.sh +++ b/scripts/review-bot-watch.sh @@ -64,6 +64,36 @@ normalize_bool() { esac } +gh_api_probe_looks_like_network_failure() { + local probe_output="$1" + [[ "$probe_output" =~ error[[:space:]]connecting[[:space:]]to[[:space:]]api\.github\.com|Could[[:space:]]not[[:space:]]resolve[[:space:]]host|could[[:space:]]not[[:space:]]resolve[[:space:]]host|Failed[[:space:]]to[[:space:]]connect|failed[[:space:]]to[[:space:]]connect|Network[[:space:]]is[[:space:]]unreachable|network[[:space:]]is[[:space:]]unreachable|Connection[[:space:]]timed[[:space:]]out|connection[[:space:]]timed[[:space:]]out|Temporary[[:space:]]failure[[:space:]]in[[:space:]]name[[:space:]]resolution|temporary[[:space:]]failure[[:space:]]in[[:space:]]name[[:space:]]resolution ]] +} + +ensure_gh_auth_or_explain() { + local auth_output probe_output + if auth_output="$(gh auth status 2>&1)"; then + return 0 + fi + + if probe_output="$(gh api user --jq .login 2>&1)"; then + echo "[review-bot-watch] gh auth status failed, but gh api user succeeded; continuing with usable GitHub auth." >&2 + return 0 + fi + + if gh_api_probe_looks_like_network_failure "$probe_output"; then + echo "[review-bot-watch] GitHub API is unreachable, so gh cannot validate the stored token." >&2 + echo "[review-bot-watch] This is a network or Codex sandbox connectivity problem, not proof that the token is invalid." >&2 + echo "$probe_output" >&2 + return 1 + fi + + echo "[review-bot-watch] gh is not authenticated. Run: gh auth login" >&2 + if [[ -n "$auth_output" ]]; then + echo "$auth_output" >&2 + fi + return 1 +} + ONCE=0 while [[ $# -gt 0 ]]; do @@ -153,8 +183,7 @@ if ! command -v codex >/dev/null 2>&1; then exit 127 fi -if ! gh auth status >/dev/null 2>&1; then - echo "[review-bot-watch] gh is not authenticated. Run: gh auth login" >&2 +if ! ensure_gh_auth_or_explain; then exit 1 fi diff --git a/src/cli/main.js b/src/cli/main.js index 21d12fad..4934fbdb 100755 --- a/src/cli/main.js +++ b/src/cli/main.js @@ -3326,6 +3326,25 @@ function renderGeneratedReleaseNotes(entries, currentTag, previousTag) { return `GitGuardex ${currentTag}\n\n${intro}\n\n${sections}`; } +function describeGhAuthFailure(ghBin, authStatus) { + if (authStatus.error) { + return `unable to run '${ghBin} auth status': ${authStatus.error.message}`; + } + + const authDetails = (authStatus.stderr || authStatus.stdout || '').trim(); + const apiProbe = run(ghBin, ['api', 'user', '--jq', '.login'], { timeout: 20_000 }); + if (apiProbe.status === 0) { + return ''; + } + + const apiDetails = (apiProbe.stderr || apiProbe.stdout || apiProbe.error?.message || '').trim(); + if (/error connecting to api\.github\.com|could not resolve host|failed to connect|network is unreachable|connection timed out|temporary failure in name resolution/i.test(apiDetails)) { + return `GitHub API is unreachable, so '${ghBin} auth status' cannot validate the stored token. This is a network or sandbox connectivity problem, not proof that the token is invalid.${apiDetails ? `\n${apiDetails}` : ''}`; + } + + return `'${ghBin}' auth is unavailable.${authDetails ? `\n${authDetails}` : ''}`; +} + function buildReleaseNotesFromReadme(repoRoot, currentTag, previousTag) { const readme = readRepoReadme(repoRoot); const entries = parseReadmeReleaseEntries(readme); @@ -3353,12 +3372,11 @@ function release(rawArgs) { } const ghAuthStatus = run(GH_BIN, ['auth', 'status'], { timeout: 20_000 }); - if (ghAuthStatus.error) { - throw new Error(`Release blocked: unable to run '${GH_BIN} auth status': ${ghAuthStatus.error.message}`); - } if (ghAuthStatus.status !== 0) { - const details = (ghAuthStatus.stderr || ghAuthStatus.stdout || '').trim(); - throw new Error(`Release blocked: '${GH_BIN}' auth is unavailable.${details ? `\n${details}` : ''}`); + const ghAuthFailure = describeGhAuthFailure(GH_BIN, ghAuthStatus); + if (ghAuthFailure) { + throw new Error(`Release blocked: ${ghAuthFailure}`); + } } const releasePackageJson = readReleaseRepoPackageJson(repoRoot); diff --git a/templates/scripts/review-bot-watch.sh b/templates/scripts/review-bot-watch.sh index 064a71a7..c75deb45 100755 --- a/templates/scripts/review-bot-watch.sh +++ b/templates/scripts/review-bot-watch.sh @@ -64,6 +64,36 @@ normalize_bool() { esac } +gh_api_probe_looks_like_network_failure() { + local probe_output="$1" + [[ "$probe_output" =~ error[[:space:]]connecting[[:space:]]to[[:space:]]api\.github\.com|Could[[:space:]]not[[:space:]]resolve[[:space:]]host|could[[:space:]]not[[:space:]]resolve[[:space:]]host|Failed[[:space:]]to[[:space:]]connect|failed[[:space:]]to[[:space:]]connect|Network[[:space:]]is[[:space:]]unreachable|network[[:space:]]is[[:space:]]unreachable|Connection[[:space:]]timed[[:space:]]out|connection[[:space:]]timed[[:space:]]out|Temporary[[:space:]]failure[[:space:]]in[[:space:]]name[[:space:]]resolution|temporary[[:space:]]failure[[:space:]]in[[:space:]]name[[:space:]]resolution ]] +} + +ensure_gh_auth_or_explain() { + local auth_output probe_output + if auth_output="$(gh auth status 2>&1)"; then + return 0 + fi + + if probe_output="$(gh api user --jq .login 2>&1)"; then + echo "[review-bot-watch] gh auth status failed, but gh api user succeeded; continuing with usable GitHub auth." >&2 + return 0 + fi + + if gh_api_probe_looks_like_network_failure "$probe_output"; then + echo "[review-bot-watch] GitHub API is unreachable, so gh cannot validate the stored token." >&2 + echo "[review-bot-watch] This is a network or Codex sandbox connectivity problem, not proof that the token is invalid." >&2 + echo "$probe_output" >&2 + return 1 + fi + + echo "[review-bot-watch] gh is not authenticated. Run: gh auth login" >&2 + if [[ -n "$auth_output" ]]; then + echo "$auth_output" >&2 + fi + return 1 +} + ONCE=0 while [[ $# -gt 0 ]]; do @@ -153,8 +183,7 @@ if ! command -v codex >/dev/null 2>&1; then exit 127 fi -if ! gh auth status >/dev/null 2>&1; then - echo "[review-bot-watch] gh is not authenticated. Run: gh auth login" >&2 +if ! ensure_gh_auth_or_explain; then exit 1 fi diff --git a/test/agents.test.js b/test/agents.test.js index b51ecdd9..191b2b44 100644 --- a/test/agents.test.js +++ b/test/agents.test.js @@ -84,6 +84,36 @@ test('review-bot-watch uses explicit codex-agent flags for argument parsing comp }); +test('review-bot-watch reports GitHub network failure separately from invalid auth', () => { + const repoDir = initRepo(); + seedCommit(repoDir); + const fakeGh = createFakeGhScript(` +if [[ "$1" == "auth" && "$2" == "status" ]]; then + echo "github.com" >&2 + echo " X github.com: authentication failed" >&2 + echo " - The github.com token in /home/deadpool/.config/gh/hosts.yml is no longer valid." >&2 + exit 1 +fi +if [[ "$1" == "api" && "$2" == "user" ]]; then + echo "error connecting to api.github.com" >&2 + exit 1 +fi +echo "unexpected gh args: $*" >&2 +exit 1 +`); + const fakeCodex = createFakeBin('codex', 'exit 0'); + + const result = runReviewBot(['--once'], repoDir, { + PATH: `${fakeGh.fakeBin}:${fakeCodex.fakeBin}:${process.env.PATH}`, + }); + + assert.equal(result.status, 1); + assert.match(result.stderr, /GitHub API is unreachable/); + assert.match(result.stderr, /network or Codex sandbox connectivity problem/); + assert.doesNotMatch(result.stderr, /Run: gh auth login/); +}); + + test('review command launches local review-bot script and accepts legacy start token', () => { const repoDir = initRepo(); const scriptsDir = path.join(repoDir, 'scripts'); diff --git a/test/release.test.js b/test/release.test.js index 403bc8d5..d780056d 100644 --- a/test/release.test.js +++ b/test/release.test.js @@ -257,6 +257,96 @@ exit 1 }); +test('release reports GitHub network failure separately from invalid auth', () => { + const repoDir = initRepoOnBranch('main'); + seedReleasePackageManifest(repoDir); + fs.writeFileSync( + path.join(repoDir, 'README.md'), + `## Release notes + +### v${cliVersion} +- Current release fix. +`, + 'utf8', + ); + seedCommit(repoDir); + + const fakeGh = createFakeGhScript(` +if [[ "$1" == "auth" && "$2" == "status" ]]; then + echo "github.com" >&2 + echo " X github.com: authentication failed" >&2 + echo " - The github.com token in /home/deadpool/.config/gh/hosts.yml is no longer valid." >&2 + exit 1 +fi +if [[ "$1" == "api" && "$2" == "user" ]]; then + echo "error connecting to api.github.com" >&2 + exit 1 +fi +echo "unexpected gh args: $*" >&2 +exit 1 +`); + + const result = runNodeWithEnv(['release'], repoDir, { + GUARDEX_RELEASE_REPO: repoDir, + GUARDEX_GH_BIN: fakeGh.fakePath, + }); + + assert.equal(result.status, 1); + assert.match(result.stderr, /GitHub API is unreachable/); + assert.match(result.stderr, /network or sandbox connectivity problem/); + assert.doesNotMatch(result.stderr, /Run: gh auth login/); +}); + + +test('release continues when gh api proves auth despite auth status failure', () => { + const repoDir = initRepoOnBranch('main'); + seedReleasePackageManifest(repoDir); + fs.writeFileSync( + path.join(repoDir, 'README.md'), + `## Release notes + +### v${cliVersion} +- Current release fix. +`, + 'utf8', + ); + seedCommit(repoDir); + + const markerPath = path.join(repoDir, '.gh-release-auth-fallback-called'); + const fakeGh = createFakeGhScript(` +if [[ "$1" == "auth" && "$2" == "status" ]]; then + echo "github.com auth status failed" >&2 + exit 1 +fi +if [[ "$1" == "api" && "$2" == "user" ]]; then + echo "NagyVikt" + exit 0 +fi +if [[ "$1" == "release" && "$2" == "list" ]]; then + exit 0 +fi +if [[ "$1" == "release" && "$2" == "view" ]]; then + exit 1 +fi +if [[ "$1" == "release" && "$2" == "create" ]]; then + printf '%s\\n' "$@" > "${markerPath}" + printf '%s\\n' "https://example.test/releases/tag/v${cliVersion}" + exit 0 +fi +echo "unexpected gh args: $*" >&2 +exit 1 +`); + + const result = runNodeWithEnv(['release'], repoDir, { + GUARDEX_RELEASE_REPO: repoDir, + GUARDEX_GH_BIN: fakeGh.fakePath, + }); + + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.match(fs.readFileSync(markerPath, 'utf8'), /^create$/m); +}); + + test('typo helper maps relaese/realaese to release', () => { const repoDir = initRepoOnBranch('main'); seedReleasePackageManifest(repoDir);