Skip to content
This repository was archived by the owner on Apr 30, 2026. It is now read-only.

Commit 1fbfcb9

Browse files
authored
Merge branch 'google-gemini:main' into main
2 parents c1fb07b + 3e1a377 commit 1fbfcb9

152 files changed

Lines changed: 9459 additions & 4143 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/gemini-scheduled-stale-issue-closer.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,14 @@ jobs:
7979
continue;
8080
}
8181
82-
// Skip if it has a maintainer label
83-
if (issue.labels.some(label => label.name.toLowerCase().includes('maintainer'))) {
82+
// Skip if it has a maintainer, help wanted, or Public Roadmap label
83+
const rawLabels = issue.labels.map((l) => l.name);
84+
const lowercaseLabels = rawLabels.map((l) => l.toLowerCase());
85+
if (
86+
lowercaseLabels.some((l) => l.includes('maintainer')) ||
87+
lowercaseLabels.includes('help wanted') ||
88+
rawLabels.includes('🗓️ Public Roadmap')
89+
) {
8490
continue;
8591
}
8692
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
name: 'Gemini Scheduled Stale PR Closer'
2+
3+
on:
4+
schedule:
5+
- cron: '0 2 * * *' # Every day at 2 AM UTC
6+
pull_request:
7+
types: ['opened', 'edited']
8+
workflow_dispatch:
9+
inputs:
10+
dry_run:
11+
description: 'Run in dry-run mode'
12+
required: false
13+
default: false
14+
type: 'boolean'
15+
16+
jobs:
17+
close-stale-prs:
18+
if: "github.repository == 'google-gemini/gemini-cli'"
19+
runs-on: 'ubuntu-latest'
20+
permissions:
21+
pull-requests: 'write'
22+
issues: 'write'
23+
steps:
24+
- name: 'Generate GitHub App Token'
25+
id: 'generate_token'
26+
uses: 'actions/create-github-app-token@v1'
27+
with:
28+
app-id: '${{ secrets.APP_ID }}'
29+
private-key: '${{ secrets.PRIVATE_KEY }}'
30+
owner: '${{ github.repository_owner }}'
31+
repositories: 'gemini-cli'
32+
33+
- name: 'Process Stale PRs'
34+
uses: 'actions/github-script@v7'
35+
env:
36+
DRY_RUN: '${{ inputs.dry_run }}'
37+
with:
38+
github-token: '${{ steps.generate_token.outputs.token }}'
39+
script: |
40+
const dryRun = process.env.DRY_RUN === 'true';
41+
const thirtyDaysAgo = new Date();
42+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
43+
44+
// 1. Fetch maintainers for verification
45+
let maintainerLogins = new Set();
46+
try {
47+
const members = await github.paginate(github.rest.teams.listMembersInOrg, {
48+
org: context.repo.owner,
49+
team_slug: 'gemini-cli-maintainers'
50+
});
51+
maintainerLogins = new Set(members.map(m => m.login));
52+
} catch (e) {
53+
core.warning('Failed to fetch team members');
54+
}
55+
56+
const isMaintainer = (login, assoc) => {
57+
if (maintainerLogins.size > 0) return maintainerLogins.has(login);
58+
return ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc);
59+
};
60+
61+
// 2. Determine which PRs to check
62+
let prs = [];
63+
if (context.eventName === 'pull_request') {
64+
const { data: pr } = await github.rest.pulls.get({
65+
owner: context.repo.owner,
66+
repo: context.repo.repo,
67+
pull_number: context.payload.pull_request.number
68+
});
69+
prs = [pr];
70+
} else {
71+
prs = await github.paginate(github.rest.pulls.list, {
72+
owner: context.repo.owner,
73+
repo: context.repo.repo,
74+
state: 'open',
75+
per_page: 100
76+
});
77+
}
78+
79+
for (const pr of prs) {
80+
const maintainerPr = isMaintainer(pr.user.login, pr.author_association);
81+
const isBot = pr.user.type === 'Bot' || pr.user.login.endsWith('[bot]');
82+
83+
// Detection Logic for Linked Issues
84+
// Check 1: Official GitHub "Closing Issue" link (GraphQL)
85+
const linkedIssueQuery = `query($owner:String!, $repo:String!, $number:Int!) {
86+
repository(owner:$owner, name:$repo) {
87+
pullRequest(number:$number) {
88+
closingIssuesReferences(first: 1) { totalCount }
89+
}
90+
}
91+
}`;
92+
93+
let hasClosingLink = false;
94+
try {
95+
const res = await github.graphql(linkedIssueQuery, {
96+
owner: context.repo.owner, repo: context.repo.repo, number: pr.number
97+
});
98+
hasClosingLink = res.repository.pullRequest.closingIssuesReferences.totalCount > 0;
99+
} catch (e) {}
100+
101+
// Check 2: Regex for mentions (e.g., "Related to #123", "Part of #123", "#123")
102+
// We check for # followed by numbers or direct URLs to issues.
103+
const body = pr.body || '';
104+
const mentionRegex = /(?:#|https:\/\/github\.com\/[^\/]+\/[^\/]+\/issues\/)(\d+)/i;
105+
const hasMentionLink = mentionRegex.test(body);
106+
107+
const hasLinkedIssue = hasClosingLink || hasMentionLink;
108+
109+
// Logic for Closed PRs (Auto-Reopen)
110+
if (pr.state === 'closed' && context.eventName === 'pull_request' && context.payload.action === 'edited') {
111+
if (hasLinkedIssue) {
112+
core.info(`PR #${pr.number} now has a linked issue. Reopening.`);
113+
if (!dryRun) {
114+
await github.rest.pulls.update({
115+
owner: context.repo.owner,
116+
repo: context.repo.repo,
117+
pull_number: pr.number,
118+
state: 'open'
119+
});
120+
await github.rest.issues.createComment({
121+
owner: context.repo.owner,
122+
repo: context.repo.repo,
123+
issue_number: pr.number,
124+
body: "Thank you for linking an issue! This pull request has been automatically reopened."
125+
});
126+
}
127+
}
128+
continue;
129+
}
130+
131+
// Logic for Open PRs (Immediate Closure)
132+
if (pr.state === 'open' && !maintainerPr && !hasLinkedIssue && !isBot) {
133+
core.info(`PR #${pr.number} is missing a linked issue. Closing.`);
134+
if (!dryRun) {
135+
await github.rest.issues.createComment({
136+
owner: context.repo.owner,
137+
repo: context.repo.repo,
138+
issue_number: pr.number,
139+
body: "Hi there! Thank you for your contribution to Gemini CLI. \n\nTo improve our contribution process and better track changes, we now require all pull requests to be associated with an existing issue, as announced in our [recent discussion](https://github.com/google-gemini/gemini-cli/discussions/16706) and as detailed in our [CONTRIBUTING.md](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md#1-link-to-an-existing-issue).\n\nThis pull request is being closed because it is not currently linked to an issue. **Once you have updated the description of this PR to link an issue (e.g., by adding `Fixes #123` or `Related to #123`), it will be automatically reopened.**\n\n**How to link an issue:**\nAdd a keyword followed by the issue number (e.g., `Fixes #123`) in the description of your pull request. For more details on supported keywords and how linking works, please refer to the [GitHub Documentation on linking pull requests to issues](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).\n\nThank you for your understanding and for being a part of our community!"
140+
});
141+
await github.rest.pulls.update({
142+
owner: context.repo.owner,
143+
repo: context.repo.repo,
144+
pull_number: pr.number,
145+
state: 'closed'
146+
});
147+
}
148+
continue;
149+
}
150+
151+
// Staleness check (Scheduled runs only)
152+
if (pr.state === 'open' && context.eventName !== 'pull_request') {
153+
const labels = pr.labels.map(l => l.name.toLowerCase());
154+
if (labels.includes('help wanted') || labels.includes('🔒 maintainer only')) continue;
155+
156+
let lastActivity = new Date(0);
157+
try {
158+
const reviews = await github.paginate(github.rest.pulls.listReviews, {
159+
owner: context.repo.owner,
160+
repo: context.repo.repo,
161+
pull_number: pr.number
162+
});
163+
for (const r of reviews) {
164+
if (isMaintainer(r.user.login, r.author_association)) {
165+
const d = new Date(r.submitted_at || r.updated_at);
166+
if (d > lastActivity) lastActivity = d;
167+
}
168+
}
169+
const comments = await github.paginate(github.rest.issues.listComments, {
170+
owner: context.repo.owner,
171+
repo: context.repo.repo,
172+
issue_number: pr.number
173+
});
174+
for (const c of comments) {
175+
if (isMaintainer(c.user.login, c.author_association)) {
176+
const d = new Date(c.updated_at);
177+
if (d > lastActivity) lastActivity = d;
178+
}
179+
}
180+
} catch (e) {}
181+
182+
if (maintainerPr) {
183+
const d = new Date(pr.created_at);
184+
if (d > lastActivity) lastActivity = d;
185+
}
186+
187+
if (lastActivity < thirtyDaysAgo) {
188+
core.info(`PR #${pr.number} is stale.`);
189+
if (!dryRun) {
190+
await github.rest.issues.createComment({
191+
owner: context.repo.owner,
192+
repo: context.repo.repo,
193+
issue_number: pr.number,
194+
body: "Hi there! Thank you for your contribution to Gemini CLI. We really appreciate the time and effort you've put into this pull request.\n\nTo keep our backlog manageable and ensure we're focusing on current priorities, we are closing pull requests that haven't seen maintainer activity for 30 days. Currently, the team is prioritizing work associated with **🔒 maintainer only** or **help wanted** issues.\n\nIf you believe this change is still critical, please feel free to comment with updated details. Otherwise, we encourage contributors to focus on open issues labeled as **help wanted**. Thank you for your understanding!"
195+
});
196+
await github.rest.pulls.update({
197+
owner: context.repo.owner,
198+
repo: context.repo.repo,
199+
pull_number: pr.number,
200+
state: 'closed'
201+
});
202+
}
203+
}
204+
}
205+
}

.github/workflows/label-enforcer.yml

Lines changed: 0 additions & 119 deletions
This file was deleted.

.github/workflows/stale.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,5 @@ jobs:
4040
If this is still relevant, you are welcome to reopen or leave a comment. Thanks for contributing!
4141
days-before-stale: 60
4242
days-before-close: 14
43-
exempt-issue-labels: 'pinned,security'
44-
exempt-pr-labels: 'pinned,security'
43+
exempt-issue-labels: 'pinned,security,🔒 maintainer only,help wanted,🗓️ Public Roadmap'
44+
exempt-pr-labels: 'pinned,security,🔒 maintainer only,help wanted,🗓️ Public Roadmap'

GEMINI.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ powerful tool for developers.
6262
- **Imports:** Use specific imports and avoid restricted relative imports
6363
between packages (enforced by ESLint).
6464

65+
## Testing Conventions
66+
67+
- **Environment Variables:** When testing code that depends on environment
68+
variables, use `vi.stubEnv('NAME', 'value')` in `beforeEach` and
69+
`vi.unstubAllEnvs()` in `afterEach`. Avoid modifying `process.env` directly as
70+
it can lead to test leakage and is less reliable. To "unset" a variable, use
71+
an empty string `vi.stubEnv('NAME', '')`.
72+
6573
## Documentation
6674

6775
- Suggest documentation updates when code changes render existing documentation

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,23 @@ npm install -g @google/gemini-cli
5555
brew install gemini-cli
5656
```
5757

58+
#### Install globally with MacPorts (macOS)
59+
60+
```bash
61+
sudo port install gemini-cli
62+
```
63+
64+
#### Install with Anaconda (for restricted environments)
65+
66+
```bash
67+
# Create and activate a new environment
68+
conda create -y -n gemini_env -c conda-forge nodejs
69+
conda activate gemini_env
70+
71+
# Install Gemini CLI globally via npm (inside the environment)
72+
npm install -g @google/gemini-cli
73+
```
74+
5875
## Release Cadence and Tags
5976

6077
See [Releases](./docs/releases.md) for more details.

0 commit comments

Comments
 (0)