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
33 changes: 31 additions & 2 deletions scripts/review-bot-watch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
28 changes: 23 additions & 5 deletions src/cli/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
33 changes: 31 additions & 2 deletions templates/scripts/review-bot-watch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
30 changes: 30 additions & 0 deletions test/agents.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
90 changes: 90 additions & 0 deletions test/release.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading