Skip to content

Commit df9ffca

Browse files
committed
Merge branch 'main' of https://github.com/strands-agents/sdk-python into gitikavj/add-gemini-model-provider
2 parents c755b75 + 500d01a commit df9ffca

File tree

4 files changed

+330
-151
lines changed

4 files changed

+330
-151
lines changed

.github/workflows/auto-close.yml

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
name: Auto Close Issues
2+
3+
on:
4+
schedule:
5+
- cron: '0 14 * * 1-5' # 9 AM EST (2 PM UTC) Monday through Friday
6+
workflow_dispatch:
7+
inputs:
8+
dry_run:
9+
description: 'Run in dry-run mode (no actions taken, only logging)'
10+
required: false
11+
default: 'false'
12+
type: boolean
13+
14+
jobs:
15+
auto-close:
16+
runs-on: ubuntu-latest
17+
strategy:
18+
matrix:
19+
include:
20+
- label: 'autoclose in 3 days'
21+
days: 3
22+
issue_types: 'issues' #issues/pulls/both
23+
replacement_label: ''
24+
closure_message: 'This issue has been automatically closed as it was marked for auto-closure by the team and no additional responses was received within 3 days.'
25+
dry_run: 'false'
26+
- label: 'autoclose in 7 days'
27+
days: 7
28+
issue_types: 'issues' # issues/pulls/both
29+
replacement_label: ''
30+
closure_message: 'This issue has been automatically closed as it was marked for auto-closure by the team and no additional responses was received within 7 days.'
31+
dry_run: 'false'
32+
steps:
33+
- name: Validate and process ${{ matrix.label }}
34+
uses: actions/github-script@v8
35+
env:
36+
LABEL_NAME: ${{ matrix.label }}
37+
DAYS_TO_WAIT: ${{ matrix.days }}
38+
AUTHORIZED_USERS: ''
39+
AUTH_MODE: 'write-access'
40+
ISSUE_TYPES: ${{ matrix.issue_types }}
41+
DRY_RUN: ${{ matrix.dry_run }}
42+
REPLACEMENT_LABEL: ${{ matrix.replacement_label }}
43+
CLOSE_MESSAGE: ${{matrix.closure_message}}
44+
with:
45+
script: |
46+
const REQUIRED_PERMISSIONS = ['write', 'admin'];
47+
const CLOSE_MESSAGE = process.env.CLOSE_MESSAGE;
48+
const isDryRun = '${{ inputs.dry_run }}' === 'true' || process.env.DRY_RUN === 'true';
49+
50+
const config = {
51+
labelName: process.env.LABEL_NAME,
52+
daysToWait: parseInt(process.env.DAYS_TO_WAIT),
53+
authMode: process.env.AUTH_MODE,
54+
authorizedUsers: process.env.AUTHORIZED_USERS?.split(',').map(u => u.trim()).filter(u => u) || [],
55+
issueTypes: process.env.ISSUE_TYPES,
56+
replacementLabel: process.env.REPLACEMENT_LABEL?.trim() || null
57+
};
58+
59+
console.log(`🏷️ Processing label: "${config.labelName}" (${config.daysToWait} days)`);
60+
if (isDryRun) console.log('🧪 DRY-RUN MODE: No actions will be taken');
61+
62+
const cutoffDate = new Date();
63+
cutoffDate.setDate(cutoffDate.getDate() - config.daysToWait);
64+
65+
async function isAuthorizedUser(username) {
66+
try {
67+
if (config.authMode === 'users') {
68+
return config.authorizedUsers.includes(username);
69+
} else if (config.authMode === 'write-access') {
70+
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
71+
owner: context.repo.owner,
72+
repo: context.repo.repo,
73+
username: username
74+
});
75+
return REQUIRED_PERMISSIONS.includes(data.permission);
76+
}
77+
} catch (error) {
78+
console.log(`⚠️ Failed to check authorization for ${username}: ${error.message}`);
79+
return false;
80+
}
81+
return false;
82+
}
83+
84+
let allIssues = [];
85+
let page = 1;
86+
87+
while (true) {
88+
const { data: issues } = await github.rest.issues.listForRepo({
89+
owner: context.repo.owner,
90+
repo: context.repo.repo,
91+
state: 'open',
92+
labels: config.labelName,
93+
sort: 'updated',
94+
direction: 'desc',
95+
per_page: 100,
96+
page: page
97+
});
98+
99+
if (issues.length === 0) break;
100+
allIssues = allIssues.concat(issues);
101+
if (issues.length < 100) break;
102+
page++;
103+
}
104+
105+
const targetIssues = allIssues.filter(issue => {
106+
if (config.issueTypes === 'issues' && issue.pull_request) return false;
107+
if (config.issueTypes === 'pulls' && !issue.pull_request) return false;
108+
return true;
109+
});
110+
111+
console.log(`🔍 Found ${targetIssues.length} items with label "${config.labelName}"`);
112+
113+
if (targetIssues.length === 0) {
114+
console.log('✅ No items to process');
115+
return;
116+
}
117+
118+
let closedCount = 0;
119+
let labelRemovedCount = 0;
120+
let skippedCount = 0;
121+
122+
for (const issue of targetIssues) {
123+
console.log(`\n📋 Processing #${issue.number}: ${issue.title}`);
124+
125+
try {
126+
const { data: events } = await github.rest.issues.listEvents({
127+
owner: context.repo.owner,
128+
repo: context.repo.repo,
129+
issue_number: issue.number
130+
});
131+
132+
const labelEvents = events
133+
.filter(e => e.event === 'labeled' && e.label?.name === config.labelName)
134+
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
135+
136+
if (labelEvents.length === 0) {
137+
console.log(`⚠️ No label events found for #${issue.number}`);
138+
skippedCount++;
139+
continue;
140+
}
141+
142+
const lastLabelAdded = new Date(labelEvents[0].created_at);
143+
const labelAdder = labelEvents[0].actor.login;
144+
145+
const { data: comments } = await github.rest.issues.listComments({
146+
owner: context.repo.owner,
147+
repo: context.repo.repo,
148+
issue_number: issue.number,
149+
since: lastLabelAdded.toISOString()
150+
});
151+
152+
let hasUnauthorizedComment = false;
153+
154+
for (const comment of comments) {
155+
if (comment.user.login === labelAdder) continue;
156+
157+
const isAuthorized = await isAuthorizedUser(comment.user.login);
158+
if (!isAuthorized) {
159+
console.log(`❌ New comment from ${comment.user.login}`);
160+
hasUnauthorizedComment = true;
161+
break;
162+
}
163+
}
164+
165+
if (hasUnauthorizedComment) {
166+
if (isDryRun) {
167+
console.log(`🧪 DRY-RUN: Would remove ${config.labelName} label from #${issue.number}`);
168+
if (config.replacementLabel) {
169+
console.log(`🧪 DRY-RUN: Would add ${config.replacementLabel} label to #${issue.number}`);
170+
}
171+
} else {
172+
await github.rest.issues.removeLabel({
173+
owner: context.repo.owner,
174+
repo: context.repo.repo,
175+
issue_number: issue.number,
176+
name: config.labelName
177+
});
178+
console.log(`🏷️ Removed ${config.labelName} label from #${issue.number}`);
179+
180+
if (config.replacementLabel) {
181+
await github.rest.issues.addLabels({
182+
owner: context.repo.owner,
183+
repo: context.repo.repo,
184+
issue_number: issue.number,
185+
labels: [config.replacementLabel]
186+
});
187+
console.log(`🏷️ Added ${config.replacementLabel} label to #${issue.number}`);
188+
}
189+
}
190+
labelRemovedCount++;
191+
continue;
192+
}
193+
194+
if (lastLabelAdded > cutoffDate) {
195+
const daysRemaining = Math.ceil((lastLabelAdded - cutoffDate) / (1000 * 60 * 60 * 24));
196+
console.log(`⏳ Label added too recently (${daysRemaining} days remaining)`);
197+
skippedCount++;
198+
continue;
199+
}
200+
201+
if (isDryRun) {
202+
console.log(`🧪 DRY-RUN: Would close #${issue.number} with comment`);
203+
} else {
204+
await github.rest.issues.createComment({
205+
owner: context.repo.owner,
206+
repo: context.repo.repo,
207+
issue_number: issue.number,
208+
body: CLOSE_MESSAGE
209+
});
210+
211+
await github.rest.issues.update({
212+
owner: context.repo.owner,
213+
repo: context.repo.repo,
214+
issue_number: issue.number,
215+
state: 'closed'
216+
});
217+
218+
console.log(`🔒 Closed #${issue.number}`);
219+
}
220+
closedCount++;
221+
} catch (error) {
222+
console.log(`❌ Error processing #${issue.number}: ${error.message}`);
223+
skippedCount++;
224+
}
225+
}
226+
227+
console.log(`\n📊 Summary for "${config.labelName}":`);
228+
if (isDryRun) {
229+
console.log(` 🧪 DRY-RUN MODE - No actual changes made:`);
230+
console.log(` • Issues that would be closed: ${closedCount}`);
231+
console.log(` • Labels that would be removed: ${labelRemovedCount}`);
232+
} else {
233+
console.log(` • Issues closed: ${closedCount}`);
234+
console.log(` • Labels removed: ${labelRemovedCount}`);
235+
}
236+
console.log(` • Issues skipped: ${skippedCount}`);
237+
console.log(` • Total processed: ${targetIssues.length}`);

.pre-commit-config.yaml

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ repos:
33
hooks:
44
- id: hatch-format
55
name: Format code
6-
entry: hatch fmt --formatter
6+
entry: hatch run test-format
77
language: system
88
pass_filenames: false
99
types: [python]
@@ -15,13 +15,6 @@ repos:
1515
pass_filenames: false
1616
types: [python]
1717
stages: [pre-commit]
18-
- id: hatch-test-lint
19-
name: Type linting
20-
entry: hatch run test-lint
21-
language: system
22-
pass_filenames: false
23-
types: [ python ]
24-
stages: [ pre-commit ]
2518
- id: hatch-test
2619
name: Unit tests
2720
entry: hatch test

CONTRIBUTING.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,7 @@ This project uses [hatchling](https://hatch.pypa.io/latest/build/#hatchling) as
4444

4545
1. Entering virtual environment using `hatch` (recommended), then launch your IDE in the new shell.
4646
```bash
47-
hatch shell dev
48-
```
49-
50-
Alternatively, install development dependencies in a manually created virtual environment:
51-
```bash
52-
pip install -e ".[all]"
47+
hatch shell
5348
```
5449

5550

@@ -73,6 +68,10 @@ This project uses [hatchling](https://hatch.pypa.io/latest/build/#hatchling) as
7368
```bash
7469
hatch test
7570
```
71+
Or run them with coverage:
72+
```bash
73+
hatch test -c
74+
```
7675

7776
6. Run integration tests:
7877
```bash

0 commit comments

Comments
 (0)