feat: open repo secrets page in addition to README (#5684)

This commit is contained in:
Lee James 2025-08-07 12:00:46 -04:00 committed by GitHub
parent 6ae75c9f32
commit 8d848dca4a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 158 additions and 4 deletions

View File

@ -18,6 +18,7 @@ vi.mock('../../utils/gitUtils.js', () => ({
isGitHubRepository: vi.fn(),
getGitRepoRoot: vi.fn(),
getLatestGitHubRelease: vi.fn(),
getGitHubRepoInfo: vi.fn(),
}));
describe('setupGithubCommand', async () => {
@ -30,7 +31,9 @@ describe('setupGithubCommand', async () => {
});
it('returns a tool action to download github workflows and handles paths', async () => {
const fakeRepoRoot = '/github.com/fake/repo/root';
const fakeRepoOwner = 'fake';
const fakeRepoName = 'repo';
const fakeRepoRoot = `/github.com/${fakeRepoOwner}/${fakeRepoName}/root`;
const fakeReleaseVersion = 'v1.2.3';
vi.mocked(gitUtils.isGitHubRepository).mockReturnValueOnce(true);
@ -38,6 +41,10 @@ describe('setupGithubCommand', async () => {
vi.mocked(gitUtils.getLatestGitHubRelease).mockResolvedValueOnce(
fakeReleaseVersion,
);
vi.mocked(gitUtils.getGitHubRepoInfo).mockReturnValue({
owner: fakeRepoOwner,
repo: fakeRepoName,
});
const result = (await setupGithubCommand.action?.(
{} as CommandContext,

View File

@ -11,6 +11,7 @@ import {
getGitRepoRoot,
getLatestGitHubRelease,
isGitHubRepository,
getGitHubRepoInfo,
} from '../../utils/gitUtils.js';
import {
@ -18,6 +19,27 @@ import {
SlashCommand,
SlashCommandActionReturn,
} from './types.js';
import { getUrlOpenCommand } from '../../ui/utils/commandUtils.js';
// 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
const openCmd = getUrlOpenCommand();
// Build a list of URLs to open
const urlsToOpen = [readmeUrl];
const repoInfo = getGitHubRepoInfo();
if (repoInfo) {
urlsToOpen.push(
`https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/secrets/actions`,
);
}
// Create and join the individual commands
const commands = urlsToOpen.map((url) => `${openCmd} "${url}"`);
return commands;
}
export const setupGithubCommand: SlashCommand = {
name: 'setup-github',
@ -71,11 +93,14 @@ export const setupGithubCommand: SlashCommand = {
commands.push(curlCommand);
}
const readmeUrl = `https://github.com/google-github-actions/run-gemini-cli/blob/${releaseTag}/README.md#quick-start`;
commands.push(
`echo "Successfully downloaded ${workflows.length} workflows. Follow the steps in https://github.com/google-github-actions/run-gemini-cli/blob/${releaseTag}/README.md#quick-start (skipping the /setup-github step) to complete setup."`,
`open https://github.com/google-github-actions/run-gemini-cli/blob/${releaseTag}/README.md#quick-start`,
`echo "Successfully downloaded ${workflows.length} workflows. Follow the steps in ${readmeUrl} (skipping the /setup-github step) to complete setup."`,
);
commands.push(...getOpenUrlsCommands(readmeUrl));
const command = `(${commands.join(' && ')})`;
return {
type: 'tool',

View File

@ -11,6 +11,7 @@ import {
isAtCommand,
isSlashCommand,
copyToClipboard,
getUrlOpenCommand,
} from './commandUtils.js';
// Mock child_process
@ -342,4 +343,42 @@ describe('commandUtils', () => {
});
});
});
describe('getUrlOpenCommand', () => {
describe('on macOS (darwin)', () => {
beforeEach(() => {
mockProcess.platform = 'darwin';
});
it('should return open', () => {
expect(getUrlOpenCommand()).toBe('open');
});
});
describe('on Windows (win32)', () => {
beforeEach(() => {
mockProcess.platform = 'win32';
});
it('should return start', () => {
expect(getUrlOpenCommand()).toBe('start');
});
});
describe('on Linux (linux)', () => {
beforeEach(() => {
mockProcess.platform = 'linux';
});
it('should return xdg-open', () => {
expect(getUrlOpenCommand()).toBe('xdg-open');
});
});
describe('on unmatched OS', () => {
beforeEach(() => {
mockProcess.platform = 'unmatched';
});
it('should return xdg-open', () => {
expect(getUrlOpenCommand()).toBe('xdg-open');
});
});
});
});

View File

@ -80,3 +80,27 @@ export const copyToClipboard = async (text: string): Promise<void> => {
throw new Error(`Unsupported platform: ${process.platform}`);
}
};
export const getUrlOpenCommand = (): string => {
// --- Determine the OS-specific command to open URLs ---
let openCmd: string;
switch (process.platform) {
case 'darwin':
openCmd = 'open';
break;
case 'win32':
openCmd = 'start';
break;
case 'linux':
openCmd = 'xdg-open';
break;
default:
// Default to xdg-open, which appears to be supported for the less popular operating systems.
openCmd = 'xdg-open';
console.warn(
`Unknown platform: ${process.platform}. Attempting to open URLs with: ${openCmd}.`,
);
break;
}
return openCmd;
};

View File

@ -10,6 +10,7 @@ import {
isGitHubRepository,
getGitRepoRoot,
getLatestGitHubRelease,
getGitHubRepoInfo,
} from './gitUtils.js';
vi.mock('child_process');
@ -44,6 +45,39 @@ describe('isGitHubRepository', async () => {
});
});
describe('getGitHubRepoInfo', async () => {
beforeEach(() => {
vi.resetAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('throws an error if github repo info cannot be determined', async () => {
vi.mocked(child_process.execSync).mockImplementation((): string => {
throw new Error('oops');
});
expect(() => {
getGitHubRepoInfo();
}).toThrowError(/oops/);
});
it('throws an error if owner/repo could not be determined', async () => {
vi.mocked(child_process.execSync).mockReturnValueOnce('');
expect(() => {
getGitHubRepoInfo();
}).toThrowError(/Owner & repo could not be extracted from remote URL/);
});
it('returns the owner and repo', async () => {
vi.mocked(child_process.execSync).mockReturnValueOnce(
'https://github.com/owner/repo.git ',
);
expect(getGitHubRepoInfo()).toEqual({ owner: 'owner', repo: 'repo' });
});
});
describe('getGitRepoRoot', async () => {
beforeEach(() => {
vi.resetAllMocks();

View File

@ -91,3 +91,28 @@ export const getLatestGitHubRelease = async (
);
}
};
/**
* getGitHubRepoInfo returns the owner and repository for a GitHub repo.
* @returns the owner and repository of the github repo.
* @throws error if the exec command fails.
*/
export function getGitHubRepoInfo(): { owner: string; repo: string } {
const remoteUrl = execSync('git remote get-url origin', {
encoding: 'utf-8',
}).trim();
// Matches either https://github.com/owner/repo.git or git@github.com:owner/repo.git
const match = remoteUrl.match(
/(?:https?:\/\/|git@)github\.com(?::|\/)([^/]+)\/([^/]+?)(?:\.git)?$/,
);
// If the regex fails match, throw an error.
if (!match || !match[1] || !match[2]) {
throw new Error(
`Owner & repo could not be extracted from remote URL: ${remoteUrl}`,
);
}
return { owner: match[1], repo: match[2] };
}