Skip to content

Commit a5b6026

Browse files
authored
Pipeline update: collect activity for all non-bot org members and expand bot filtering (#2)
1 parent 1d3e7cd commit a5b6026

9 files changed

Lines changed: 412 additions & 53 deletions

File tree

‎.github/workflows/sync_activity.yml‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@ on:
88
inputs:
99
days:
1010
description: 'Number of past days of activity to sync'
11+
type: string
1112
required: false
1213
default: '7'
14+
week:
15+
description: 'Optional specific ISO week to sync (e.g. 2026-W32)'
16+
type: string
17+
required: false
18+
default: ''
1319

1420
permissions:
1521
contents: write
@@ -39,7 +45,13 @@ jobs:
3945
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4046
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4147
DAYS: ${{ inputs.days || '7' }}
42-
run: dart run tool/sync_activity.dart --days "$DAYS"
48+
WEEK: ${{ inputs.week || '' }}
49+
run: |
50+
if [ -n "$WEEK" ]; then
51+
dart run tool/sync_activity.dart --week "$WEEK"
52+
else
53+
dart run tool/sync_activity.dart --days "$DAYS"
54+
fi
4355
4456
- name: Commit and push changes
4557
run: |
@@ -50,6 +62,6 @@ jobs:
5062
echo "No activity changes to commit."
5163
else
5264
git commit -m "chore: sync contributor activity data [skip ci]"
53-
git pull --rebase origin main || true
65+
git pull --rebase || true
5466
git push
5567
fi

‎docs/DATA_PIPELINE.md‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ The pipeline records 3 qualifying contribution types:
1515

1616
### Exclusions and filtering
1717

18-
- Accounts in the `@flutter/robots` GitHub team and known automation usernames defined in `tool/bots.dart` are excluded.
19-
- Members of `@flutter/googlers` and `@flutter/partners` are excluded to conserve GitHub API rate limits.
18+
Accounts in the `@flutter/robots` and `@flutter/bots` GitHub teams and known automation usernames defined in `tool/bots.dart` (along with the deleted-account placeholder `ghost`) are excluded, while all human contributors and organization members (including Maintainers) are recorded so that 12-month contribution history is available for every organization member.
2019

2120
## Dataset structure
2221

@@ -33,8 +32,7 @@ data/
3332
└── activity_summary.json
3433
```
3534

36-
- `data/activity/` holds 52 discrete weekly JSON snapshots (`YYYY-Wxx.json`). Each file records the contributions for that ISO week.
37-
- `data/activity_summary.json` holds the rolling 12-month totals per contributor, calculated directly as the sum of all active weekly files.
35+
The `data/activity/` directory holds 52 discrete weekly JSON snapshots (`YYYY-Wxx.json`), where each file records the contributions for that ISO week, and `data/activity_summary.json` holds the rolling 12-month totals per contributor calculated directly as the sum of all active weekly files.
3836

3937
## Daily execution and rolling purge
4038

@@ -47,7 +45,4 @@ graph LR
4745
Sum --> Commit["Commit updates<br/>[skip ci]"]
4846
```
4947

50-
- Each night at 02:17 UTC, the GitHub Actions workflow (`.github/workflows/sync_activity.yml`) queries the current ISO week of activity using GraphQL with cursor pagination.
51-
- The week's snapshot in `data/activity/` is updated idempotently, avoiding double-counting across runs. Reviews are deduplicated per merged pull request.
52-
- Snapshot files older than 52 weeks are pruned based on their ISO week filenames.
53-
- `data/activity_summary.json` is recalculated by summing the active weekly files with deterministically sorted keys, rolling off activity older than 12 months without requiring historical re-scans.
48+
Each night at 02:17 UTC, the GitHub Actions workflow (`.github/workflows/sync_activity.yml`) queries the current ISO week of activity using GraphQL with cursor pagination and updates the week's snapshot in `data/activity/` idempotently, avoiding double-counting across runs and deduplicating reviews per merged pull request. Snapshot files older than 52 weeks are pruned based on their ISO week filenames, and `data/activity_summary.json` is recalculated by summing the active weekly files with deterministically sorted keys, rolling off activity older than 12 months without requiring historical re-scans.

‎test/activity_sync_test.dart‎

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import 'dart:convert';
22
import 'dart:io';
3+
import 'package:http/http.dart' as http;
4+
import 'package:http/testing.dart';
35
import 'package:test/test.dart';
6+
import '../tool/github.dart';
47
import '../tool/sync_activity.dart';
58

69
void main() {
@@ -98,5 +101,157 @@ void main() {
98101
final remaining = activityDir.listSync().whereType<File>().length;
99102
expect(remaining, equals(52));
100103
});
104+
105+
test(
106+
'pruneOldActivityFiles skips invalid filenames and handles missing dir',
107+
() {
108+
final missingDir = Directory('${tempDir.path}/non_existent');
109+
expect(pruneOldActivityFiles(missingDir), equals(0));
110+
111+
final activityDir = Directory('${tempDir.path}/activity')..createSync();
112+
File('${activityDir.path}/not-a-week.json').writeAsStringSync('{}');
113+
File('${activityDir.path}/README.txt').writeAsStringSync('ignore');
114+
expect(
115+
pruneOldActivityFiles(activityDir,
116+
referenceDate: DateTime.utc(2026, 8, 3)),
117+
equals(0));
118+
});
119+
120+
test(
121+
'fetchActivityForRange aggregates PRs, deduplicated reviews, and issues while excluding bots and ghost',
122+
() async {
123+
final mockClient = MockClient((request) async {
124+
final body = jsonDecode(request.body) as Map<String, dynamic>;
125+
final variables = body['variables'] as Map<String, dynamic>;
126+
final query = variables['query'] as String;
127+
128+
if (query.contains('is:pr')) {
129+
return http.Response(
130+
jsonEncode({
131+
'data': {
132+
'search': {
133+
'pageInfo': {'hasNextPage': false, 'endCursor': null},
134+
'nodes': [
135+
{
136+
'author': {'login': 'alice'},
137+
'reviews': {
138+
'nodes': [
139+
// Self-review by author should be ignored
140+
{
141+
'author': {'login': 'alice'}
142+
},
143+
// Duplicate review by bob on the same PR should count once
144+
{
145+
'author': {'login': 'bob'}
146+
},
147+
{
148+
'author': {'login': 'bob'}
149+
},
150+
// Maintainer review should be recorded
151+
{
152+
'author': {'login': 'Piinks'}
153+
},
154+
// Bot review should be skipped
155+
{
156+
'author': {'login': 'auto-submit'}
157+
},
158+
]
159+
}
160+
},
161+
// PR by maintainer and community review on it should both be recorded
162+
{
163+
'author': {'login': 'Piinks'},
164+
'reviews': {
165+
'nodes': [
166+
{
167+
'author': {'login': 'alice'}
168+
}
169+
]
170+
}
171+
},
172+
// PR by bot should be skipped
173+
{
174+
'author': {'login': 'engine-flutter-autoroll'},
175+
'reviews': {'nodes': <Map<String, dynamic>>[]}
176+
},
177+
]
178+
}
179+
}
180+
}),
181+
200,
182+
);
183+
} else {
184+
// Issue search
185+
return http.Response(
186+
jsonEncode({
187+
'data': {
188+
'search': {
189+
'pageInfo': {'hasNextPage': false, 'endCursor': null},
190+
'nodes': [
191+
{
192+
'author': {'login': 'alice'}
193+
},
194+
{
195+
'author': {'login': 'charlie'}
196+
},
197+
// Ghost issue should be skipped
198+
{
199+
'author': {'login': 'ghost'}
200+
},
201+
{
202+
'author': {'login': 'Piinks'}
203+
},
204+
]
205+
}
206+
}
207+
}),
208+
200,
209+
);
210+
}
211+
});
212+
213+
final client = GitHubClient(token: 'test', httpClient: mockClient);
214+
final stats = await fetchActivityForRange(
215+
client,
216+
since: DateTime.utc(2026, 8, 1),
217+
until: DateTime.utc(2026, 8, 8),
218+
);
219+
220+
expect(
221+
stats['alice'],
222+
equals({
223+
'merged_prs': 1,
224+
'issues': 1,
225+
'reviews': 1,
226+
'total': 3,
227+
}));
228+
expect(
229+
stats['bob'],
230+
equals({
231+
'merged_prs': 0,
232+
'issues': 0,
233+
'reviews': 1,
234+
'total': 1,
235+
}));
236+
expect(
237+
stats['charlie'],
238+
equals({
239+
'merged_prs': 0,
240+
'issues': 1,
241+
'reviews': 0,
242+
'total': 1,
243+
}));
244+
expect(
245+
stats['Piinks'],
246+
equals({
247+
'merged_prs': 1,
248+
'issues': 1,
249+
'reviews': 1,
250+
'total': 3,
251+
}));
252+
expect(stats.containsKey('auto-submit'), isFalse);
253+
expect(stats.containsKey('engine-flutter-autoroll'), isFalse);
254+
expect(stats.containsKey('ghost'), isFalse);
255+
});
101256
});
102257
}

‎test/bots_test.dart‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
import 'dart:convert';
2+
import 'package:http/http.dart' as http;
3+
import 'package:http/testing.dart';
14
import 'package:test/test.dart';
25
import '../tool/bots.dart';
6+
import '../tool/github.dart';
37

48
void main() {
59
group('Bot Detection (isBot)', () {
10+
tearDown(resetRobotTeamCache);
11+
612
test('identifies known bots in knownBots set', () {
713
expect(isBot('dependabot'), isTrue);
814
expect(isBot('dependabot[bot]'), isTrue);
@@ -13,7 +19,9 @@ void main() {
1319
expect(isBot('github-actions'), isTrue);
1420
expect(isBot('github-actions[bot]'), isTrue);
1521
expect(isBot('engine-flutter-autoroll'), isTrue);
22+
expect(isBot('auto-submit'), isTrue);
1623
expect(isBot('auto-submit[bot]'), isTrue);
24+
expect(isBot('google-cla'), isTrue);
1725
expect(isBot('google-cla[bot]'), isTrue);
1826
expect(isBot('renovate[bot]'), isTrue);
1927
expect(isBot('DartDevtoolWorkflowBot'), isTrue);
@@ -69,5 +77,28 @@ void main() {
6977
expect(isBot('FlutterGithubBot'), isTrue);
7078
expect(isBot(' piinks '), isFalse);
7179
});
80+
81+
test(
82+
'loadRobotTeams dynamically adds members of @flutter/robots and @flutter/bots to bots',
83+
() async {
84+
final mockClient = MockClient((request) async {
85+
expect(
86+
request.url.path,
87+
anyOf(
88+
equals('/orgs/flutter/teams/robots/members'),
89+
equals('/orgs/flutter/teams/bots/members'),
90+
),
91+
);
92+
return http.Response(
93+
jsonEncode([
94+
{'login': 'dynamic-team-robot-account'}
95+
]),
96+
200,
97+
);
98+
});
99+
final gh = GitHubClient(token: 'test', httpClient: mockClient);
100+
await loadRobotTeams(gh);
101+
expect(isBot('dynamic-team-robot-account'), isTrue);
102+
});
72103
});
73104
}

0 commit comments

Comments
 (0)