summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLee James <[email protected]>2025-08-07 12:00:46 -0400
committerGitHub <[email protected]>2025-08-07 16:00:46 +0000
commit8d848dca4a52d169b3dfea2f66e7e5f69ee5e45c (patch)
treebbb82f3a1e8024e6c116c1ca3b5a5313fa31ab02
parent6ae75c9f32a968efa50857a8f24b958a58a84fd6 (diff)
feat: open repo secrets page in addition to README (#5684)
-rw-r--r--packages/cli/src/ui/commands/setupGithubCommand.test.ts9
-rw-r--r--packages/cli/src/ui/commands/setupGithubCommand.ts29
-rw-r--r--packages/cli/src/ui/utils/commandUtils.test.ts39
-rw-r--r--packages/cli/src/ui/utils/commandUtils.ts26
-rw-r--r--packages/cli/src/utils/gitUtils.test.ts34
-rw-r--r--packages/cli/src/utils/gitUtils.ts25
6 files changed, 158 insertions, 4 deletions
diff --git a/packages/cli/src/ui/commands/setupGithubCommand.test.ts b/packages/cli/src/ui/commands/setupGithubCommand.test.ts
index 6417c60a..be0a657f 100644
--- a/packages/cli/src/ui/commands/setupGithubCommand.test.ts
+++ b/packages/cli/src/ui/commands/setupGithubCommand.test.ts
@@ -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,
diff --git a/packages/cli/src/ui/commands/setupGithubCommand.ts b/packages/cli/src/ui/commands/setupGithubCommand.ts
index 1b5b3277..84d6b5af 100644
--- a/packages/cli/src/ui/commands/setupGithubCommand.ts
+++ b/packages/cli/src/ui/commands/setupGithubCommand.ts
@@ -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',
diff --git a/packages/cli/src/ui/utils/commandUtils.test.ts b/packages/cli/src/ui/utils/commandUtils.test.ts
index 4bd48cee..db333e72 100644
--- a/packages/cli/src/ui/utils/commandUtils.test.ts
+++ b/packages/cli/src/ui/utils/commandUtils.test.ts
@@ -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');
+ });
+ });
+ });
});
diff --git a/packages/cli/src/ui/utils/commandUtils.ts b/packages/cli/src/ui/utils/commandUtils.ts
index 4280388f..80ed51ae 100644
--- a/packages/cli/src/ui/utils/commandUtils.ts
+++ b/packages/cli/src/ui/utils/commandUtils.ts
@@ -27,7 +27,7 @@ export const isAtCommand = (query: string): boolean =>
*/
export const isSlashCommand = (query: string): boolean => query.startsWith('/');
-//Copies a string snippet to the clipboard for different platforms
+// Copies a string snippet to the clipboard for different platforms
export const copyToClipboard = async (text: string): Promise<void> => {
const run = (cmd: string, args: string[]) =>
new Promise<void>((resolve, reject) => {
@@ -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;
+};
diff --git a/packages/cli/src/utils/gitUtils.test.ts b/packages/cli/src/utils/gitUtils.test.ts
index 4a29f589..7a5f210c 100644
--- a/packages/cli/src/utils/gitUtils.test.ts
+++ b/packages/cli/src/utils/gitUtils.test.ts
@@ -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();
diff --git a/packages/cli/src/utils/gitUtils.ts b/packages/cli/src/utils/gitUtils.ts
index 30ca2245..f5f9cb92 100644
--- a/packages/cli/src/utils/gitUtils.ts
+++ b/packages/cli/src/utils/gitUtils.ts
@@ -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 [email protected]: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] };
+}