diff --git a/scripts/review-bot-watch.sh b/scripts/review-bot-watch.sh index 064a71a..c75deb4 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 21d12fa..4934fbd 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 064a71a..c75deb4 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 b51ecdd..191b2b4 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 403bc8d..d780056 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);