From c95cd99166200b778d537982712d182cb8ff9b34 Mon Sep 17 00:00:00 2001 From: Abhijith V Ashok Date: Sun, 25 Jan 2026 13:33:28 +0530 Subject: [PATCH 1/6] fixes #16739: checks for fork or if user is repo owner to prevent 404 while on setup-github --- .../cli/src/ui/commands/setupGithubCommand.ts | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/ui/commands/setupGithubCommand.ts b/packages/cli/src/ui/commands/setupGithubCommand.ts index 83b9531c9d4..be33c1eca91 100644 --- a/packages/cli/src/ui/commands/setupGithubCommand.ts +++ b/packages/cli/src/ui/commands/setupGithubCommand.ts @@ -8,6 +8,7 @@ import path from 'node:path'; import * as fs from 'node:fs'; import { Writable } from 'node:stream'; import { ProxyAgent } from 'undici'; +import { execSync } from 'node:child_process'; import type { CommandContext } from '../../ui/commands/types.js'; import { @@ -40,6 +41,12 @@ export const GITHUB_COMMANDS_PATHS = [ const REPO_DOWNLOAD_URL = 'https://raw.githubusercontent.com/google-github-actions/run-gemini-cli'; const SOURCE_DIR = 'examples/workflows'; + +// Verifies if github name is command-execution safe +function isValidGitHubName(name: string): boolean { + return /^[a-zA-Z0-9_.-]+$/.test(name); +} + // Generate OS-specific commands to open the GitHub pages needed for setup. function getOpenUrlsCommands(readmeUrl: string): string[] { // Determine the OS-specific command to open URLs, ex: 'open', 'xdg-open', etc @@ -48,11 +55,52 @@ function getOpenUrlsCommands(readmeUrl: string): string[] { // Build a list of URLs to open const urlsToOpen = [readmeUrl]; - const repoInfo = getGitHubRepoInfo(); - if (repoInfo) { + let repoInfo: { owner: string; repo: string } | null = null; + try { + repoInfo = getGitHubRepoInfo(); + } catch { + /* ignore */ + } + + if (!repoInfo) { + return [ + `${openCmd} "${readmeUrl}"`, + `echo "\nℹ️ Gemini CLI: Could not determine repository info. Secrets page skipped."`, + ]; + } + + //to check for fork + const forkUsername = repoInfo.owner; + + let localGitUser: string | null = null; + try { + localGitUser = execSync('git config user.name', { + encoding: 'utf8', + }).trim(); + } catch { + /* ignore */ + } + + const isOwner = + forkUsername && + localGitUser && + forkUsername.toLowerCase() === localGitUser.toLowerCase(); + + const isSafeRepo = + repoInfo && + isValidGitHubName(repoInfo.owner) && + isValidGitHubName(repoInfo.repo); + + // Only push the URL if the fork exists and isn’t upstream + if (isOwner && isSafeRepo) { urlsToOpen.push( `https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/secrets/actions`, ); + } else { + return [ + `${openCmd} "${readmeUrl}"`, + `echo "\nℹ️ Gemini CLI: Secrets page skipped. If this is a fork, ensure origin points to your fork and repository names are valid."`, + ]; } // Create and join the individual commands From 2e06f7fdca651cf997362d364712a14f04783680 Mon Sep 17 00:00:00 2001 From: Abhijith V Ashok Date: Sun, 25 Jan 2026 16:56:06 +0530 Subject: [PATCH 2/6] added seperate verification functions for github username and repo name --- .../cli/src/ui/commands/setupGithubCommand.ts | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/packages/cli/src/ui/commands/setupGithubCommand.ts b/packages/cli/src/ui/commands/setupGithubCommand.ts index be33c1eca91..70e3cd33619 100644 --- a/packages/cli/src/ui/commands/setupGithubCommand.ts +++ b/packages/cli/src/ui/commands/setupGithubCommand.ts @@ -8,7 +8,6 @@ import path from 'node:path'; import * as fs from 'node:fs'; import { Writable } from 'node:stream'; import { ProxyAgent } from 'undici'; -import { execSync } from 'node:child_process'; import type { CommandContext } from '../../ui/commands/types.js'; import { @@ -43,8 +42,14 @@ const REPO_DOWNLOAD_URL = const SOURCE_DIR = 'examples/workflows'; // Verifies if github name is command-execution safe -function isValidGitHubName(name: string): boolean { - return /^[a-zA-Z0-9_.-]+$/.test(name); +function isValidGitHubOwner(name: string): boolean { + return /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i.test(name); +} + +// does the same for repo name +function isValidGitHubRepoName(name: string): boolean { + if (name === '.' || name === '..') return false; + return /^[a-z0-9_.-]+$/i.test(name); } // Generate OS-specific commands to open the GitHub pages needed for setup. @@ -69,27 +74,13 @@ function getOpenUrlsCommands(readmeUrl: string): string[] { ]; } - //to check for fork - const forkUsername = repoInfo.owner; - - let localGitUser: string | null = null; - try { - localGitUser = execSync('git config user.name', { - encoding: 'utf8', - }).trim(); - } catch { - /* ignore */ - } - const isOwner = - forkUsername && - localGitUser && - forkUsername.toLowerCase() === localGitUser.toLowerCase(); + repoInfo.owner && + repoInfo.localUser && + repoInfo.owner.toLowerCase() === repoInfo.localUser.toLowerCase(); const isSafeRepo = - repoInfo && - isValidGitHubName(repoInfo.owner) && - isValidGitHubName(repoInfo.repo); + isValidGitHubOwner(repoInfo.owner) && isValidGitHubRepoName(repoInfo.repo); // Only push the URL if the fork exists and isn’t upstream if (isOwner && isSafeRepo) { From e4cfa22fd7654f4506a081773a6c932cfd9fe21b Mon Sep 17 00:00:00 2001 From: Abhijith V Ashok Date: Sun, 25 Jan 2026 17:31:26 +0530 Subject: [PATCH 3/6] revieweres suggestions added, used execsync --- packages/cli/src/ui/commands/setupGithubCommand.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/ui/commands/setupGithubCommand.ts b/packages/cli/src/ui/commands/setupGithubCommand.ts index 70e3cd33619..69c2578051b 100644 --- a/packages/cli/src/ui/commands/setupGithubCommand.ts +++ b/packages/cli/src/ui/commands/setupGithubCommand.ts @@ -74,10 +74,20 @@ function getOpenUrlsCommands(readmeUrl: string): string[] { ]; } + let localUser: string | undefined; + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports, no-restricted-syntax + localUser = require('node:child_process') + .execSync('git config user.name', { encoding: 'utf-8' }) + .trim(); + } catch { + // localUser will be undefined, and isOwner will be false. + } + const isOwner = repoInfo.owner && - repoInfo.localUser && - repoInfo.owner.toLowerCase() === repoInfo.localUser.toLowerCase(); + localUser && + repoInfo.owner.toLowerCase() === localUser.toLowerCase(); const isSafeRepo = isValidGitHubOwner(repoInfo.owner) && isValidGitHubRepoName(repoInfo.repo); From 49c1aa95459dfe42c1e0e4aa1598afb202239108 Mon Sep 17 00:00:00 2001 From: Abhijith V Ashok Date: Mon, 26 Jan 2026 13:21:57 +0530 Subject: [PATCH 4/6] revieweres suggestions added, used a check for Upstream to skip serets page if owner is the known upstream org --- .../cli/src/ui/commands/setupGithubCommand.ts | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/packages/cli/src/ui/commands/setupGithubCommand.ts b/packages/cli/src/ui/commands/setupGithubCommand.ts index 69c2578051b..f04f1a1823d 100644 --- a/packages/cli/src/ui/commands/setupGithubCommand.ts +++ b/packages/cli/src/ui/commands/setupGithubCommand.ts @@ -74,33 +74,22 @@ function getOpenUrlsCommands(readmeUrl: string): string[] { ]; } - let localUser: string | undefined; - try { - // eslint-disable-next-line @typescript-eslint/no-require-imports, no-restricted-syntax - localUser = require('node:child_process') - .execSync('git config user.name', { encoding: 'utf-8' }) - .trim(); - } catch { - // localUser will be undefined, and isOwner will be false. - } - - const isOwner = - repoInfo.owner && - localUser && - repoInfo.owner.toLowerCase() === localUser.toLowerCase(); - + const UpstreamRepo = 'google-gemini/gemini-cli'; + const isUpstream = + `${repoInfo.owner}/${repoInfo.repo}`.toLowerCase() === UpstreamRepo; const isSafeRepo = isValidGitHubOwner(repoInfo.owner) && isValidGitHubRepoName(repoInfo.repo); - // Only push the URL if the fork exists and isn’t upstream - if (isOwner && isSafeRepo) { + // Only push the secrets URL if we are NOT in the upstream repo and the name is valid + if (!isUpstream && isSafeRepo) { urlsToOpen.push( `https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/secrets/actions`, ); } else { + // Skip secrets page if in upstream repo or invalid names return [ `${openCmd} "${readmeUrl}"`, - `echo "\nℹ️ Gemini CLI: Secrets page skipped. If this is a fork, ensure origin points to your fork and repository names are valid."`, + `echo "\nℹ️ Gemini CLI: Secrets page skipped. (You are in the upstream repo or the repo info is invalid)."`, ]; } From 7547182ad11b690d89c72766ae0dee5bcc28ecec Mon Sep 17 00:00:00 2001 From: Abhijith V Ashok Date: Mon, 2 Feb 2026 16:15:25 +0530 Subject: [PATCH 5/6] Triggering CI build From ed3d1201e9d9c23778d93d2a4d08763dc20aacc2 Mon Sep 17 00:00:00 2001 From: Abhijith V Ashok Date: Sun, 15 Feb 2026 13:45:26 +0530 Subject: [PATCH 6/6] added unit tests for the functions i added using gemini-cli --- .../ui/commands/setupGithubCommand.test.ts | 106 ++++++++++++++++++ .../cli/src/ui/commands/setupGithubCommand.ts | 6 +- 2 files changed, 109 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/ui/commands/setupGithubCommand.test.ts b/packages/cli/src/ui/commands/setupGithubCommand.test.ts index 0125ae70bd5..f326fa653fe 100644 --- a/packages/cli/src/ui/commands/setupGithubCommand.test.ts +++ b/packages/cli/src/ui/commands/setupGithubCommand.test.ts @@ -14,6 +14,9 @@ import { setupGithubCommand, updateGitignore, GITHUB_WORKFLOW_PATHS, + isValidGitHubOwner, + isValidGitHubRepoName, + getOpenUrlsCommands, } from './setupGithubCommand.js'; import type { CommandContext } from './types.js'; import * as commandUtils from '../utils/commandUtils.js'; @@ -339,3 +342,106 @@ describe('updateGitignore', () => { consoleSpy.mockRestore(); }); }); + +describe('isValidGitHubOwner', () => { + it('returns true for valid owner names', () => { + expect(isValidGitHubOwner('google')).toBe(true); + expect(isValidGitHubOwner('google-github')).toBe(true); + expect(isValidGitHubOwner('abc-123')).toBe(true); + expect(isValidGitHubOwner('a')).toBe(true); + expect(isValidGitHubOwner('1')).toBe(true); + expect(isValidGitHubOwner('A-b-C-1-2-3')).toBe(true); + }); + + it('returns false for invalid owner names', () => { + expect(isValidGitHubOwner('-google')).toBe(false); // Starts with hyphen + expect(isValidGitHubOwner('google-')).toBe(false); // Ends with hyphen + expect(isValidGitHubOwner('google--github')).toBe(false); // Multiple hyphens + expect(isValidGitHubOwner('google_github')).toBe(false); // Underscore + expect(isValidGitHubOwner('google.github')).toBe(false); // Dot + expect( + isValidGitHubOwner( + 'VeryLongNameThatIsOverThirtyNineCharactersLongAndShouldFail', + ), + ).toBe(false); // Too long + }); +}); + +describe('isValidGitHubRepoName', () => { + it('returns true for valid repo names', () => { + expect(isValidGitHubRepoName('gemini-cli')).toBe(true); + expect(isValidGitHubRepoName('Gemini_CLI')).toBe(true); + expect(isValidGitHubRepoName('repo.name')).toBe(true); + expect(isValidGitHubRepoName('123')).toBe(true); + expect(isValidGitHubRepoName('a')).toBe(true); + }); + + it('returns false for invalid repo names', () => { + expect(isValidGitHubRepoName('.')).toBe(false); + expect(isValidGitHubRepoName('..')).toBe(false); + expect(isValidGitHubRepoName('repo/name')).toBe(false); + expect(isValidGitHubRepoName('repo\\name')).toBe(false); + expect(isValidGitHubRepoName('repo name')).toBe(false); + }); +}); + +describe('getOpenUrlsCommands', () => { + const readmeUrl = 'https://readme.url'; + + it('returns only readme URL if repo info cannot be determined', () => { + vi.mocked(gitUtils.getGitHubRepoInfo).mockImplementationOnce(() => { + throw new Error('No repo info'); + }); + vi.mocked(commandUtils.getUrlOpenCommand).mockReturnValue('open'); + + const result = getOpenUrlsCommands(readmeUrl); + expect(result).toHaveLength(2); + expect(result[0]).toBe('open "https://readme.url"'); + expect(result[1]).toContain('Could not determine repository info'); + }); + + it('returns only readme URL if in upstream repository', () => { + vi.mocked(gitUtils.getGitHubRepoInfo).mockReturnValue({ + owner: 'google-gemini', + repo: 'gemini-cli', + }); + vi.mocked(commandUtils.getUrlOpenCommand).mockReturnValue('open'); + + const result = getOpenUrlsCommands(readmeUrl); + expect(result).toHaveLength(2); + expect(result[0]).toBe('open "https://readme.url"'); + expect(result[1]).toContain( + 'Secrets page skipped. (You are in the upstream repo', + ); + }); + + it('returns only readme URL if repo info is invalid', () => { + vi.mocked(gitUtils.getGitHubRepoInfo).mockReturnValue({ + owner: '-invalid-', + repo: 'repo', + }); + vi.mocked(commandUtils.getUrlOpenCommand).mockReturnValue('open'); + + const result = getOpenUrlsCommands(readmeUrl); + expect(result).toHaveLength(2); + expect(result[0]).toBe('open "https://readme.url"'); + expect(result[1]).toContain( + 'Secrets page skipped. (You are in the upstream repo or the repo info is invalid)', + ); + }); + + it('returns both readme and secrets URL for a safe, non-upstream repo', () => { + vi.mocked(gitUtils.getGitHubRepoInfo).mockReturnValue({ + owner: 'my-org', + repo: 'my-repo', + }); + vi.mocked(commandUtils.getUrlOpenCommand).mockReturnValue('open'); + + const result = getOpenUrlsCommands(readmeUrl); + expect(result).toHaveLength(2); + expect(result[0]).toBe('open "https://readme.url"'); + expect(result[1]).toBe( + 'open "https://github.com/my-org/my-repo/settings/secrets/actions"', + ); + }); +}); diff --git a/packages/cli/src/ui/commands/setupGithubCommand.ts b/packages/cli/src/ui/commands/setupGithubCommand.ts index f04f1a1823d..4b8812a137d 100644 --- a/packages/cli/src/ui/commands/setupGithubCommand.ts +++ b/packages/cli/src/ui/commands/setupGithubCommand.ts @@ -42,18 +42,18 @@ const REPO_DOWNLOAD_URL = const SOURCE_DIR = 'examples/workflows'; // Verifies if github name is command-execution safe -function isValidGitHubOwner(name: string): boolean { +export function isValidGitHubOwner(name: string): boolean { return /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i.test(name); } // does the same for repo name -function isValidGitHubRepoName(name: string): boolean { +export function isValidGitHubRepoName(name: string): boolean { if (name === '.' || name === '..') return false; return /^[a-z0-9_.-]+$/i.test(name); } // Generate OS-specific commands to open the GitHub pages needed for setup. -function getOpenUrlsCommands(readmeUrl: string): string[] { +export function getOpenUrlsCommands(readmeUrl: string): string[] { // Determine the OS-specific command to open URLs, ex: 'open', 'xdg-open', etc const openCmd = getUrlOpenCommand();