1
+ require "csv"
2
+ require "octokit"
3
+ require 'optparse'
4
+ require 'optparse/date'
5
+
6
+ class InactiveMemberSearch
7
+ attr_accessor :organization , :members , :repositories , :date , :unrecognized_authors
8
+
9
+ SCOPES = [ "read:org" , "read:user" , "repo" , "user:email" ]
10
+
11
+ def initialize ( options = { } )
12
+ @client = options [ :client ]
13
+ if options [ :check ]
14
+ check_app
15
+ check_scopes
16
+ check_rate_limit
17
+ exit 0
18
+ end
19
+
20
+ raise ( OptionParser ::MissingArgument ) if (
21
+ options [ :organization ] . nil? or
22
+ options [ :date ] . nil?
23
+ )
24
+
25
+ @date = options [ :date ]
26
+ @organization = options [ :organization ]
27
+ @email = options [ :email ]
28
+ @unrecognized_authors = [ ]
29
+
30
+ organization_members
31
+ organization_repositories
32
+ member_activity
33
+ end
34
+
35
+ def check_app
36
+ info "Application client/secret? #{ @client . application_authenticated? } \n "
37
+ info "Authentication Token? #{ @client . token_authenticated? } \n "
38
+ end
39
+
40
+ def check_scopes
41
+ info "Scopes: #{ @client . scopes . join ',' } \n "
42
+ end
43
+
44
+ def check_rate_limit
45
+ info "Rate limit: #{ @client . rate_limit . remaining } /#{ @client . rate_limit . limit } \n "
46
+ end
47
+
48
+ def env_help
49
+ output = <<-EOM
50
+ Required Environment variables:
51
+ OCTOKIT_ACCESS_TOKEN: A valid personal access token with Organzation admin priviliges
52
+ OCTOKIT_API_ENDPOINT: A valid GitHub/GitHub Enterprise API endpoint URL (Defaults to https://api.github.com)
53
+ EOM
54
+ output
55
+ end
56
+
57
+ # helper to get an auth token for the OAuth application and a user
58
+ def get_auth_token ( login , password , otp )
59
+ temp_client = Octokit ::Client . new ( login : login , password : password )
60
+ res = temp_client . create_authorization (
61
+ {
62
+ :idempotent => true ,
63
+ :scopes => SCOPES ,
64
+ :headers => { 'X-GitHub-OTP' => otp }
65
+ } )
66
+ res [ :token ]
67
+ end
68
+ private
69
+ def debug ( message )
70
+ $stderr. print message
71
+ end
72
+
73
+ def info ( message )
74
+ $stdout. print message
75
+ end
76
+
77
+ def member_email ( login )
78
+ @email ? @client . user ( login ) [ :email ] : ""
79
+ end
80
+
81
+ def organization_members
82
+ # get all organization members and place into an array of hashes
83
+ info "Finding #{ @organization } members "
84
+ @members = @client . organization_members ( @organization ) . collect do |m |
85
+ email =
86
+ {
87
+ login : m [ "login" ] ,
88
+ email : member_email ( m [ :login ] ) ,
89
+ active : false
90
+ }
91
+ end
92
+ info "#{ @members . length } members found.\n "
93
+ end
94
+
95
+ def organization_repositories
96
+ info "Gathering a list of repositories..."
97
+ # get all repos in the organizaton and place into a hash
98
+ @repositories = @client . organization_repositories ( @organization ) . collect do |repo |
99
+ repo [ "full_name" ]
100
+ end
101
+ info "#{ @repositories . length } repositories discovered\n "
102
+ end
103
+
104
+ def add_unrecognized_author ( author )
105
+ @unrecognized_authors << author
106
+ end
107
+
108
+ # method to switch member status to active
109
+ def make_active ( login )
110
+ hsh = @members . find { |member | member [ :login ] == login }
111
+ hsh [ :active ] = true
112
+ end
113
+
114
+ def commit_activity ( repo )
115
+ # get all commits after specified date and iterate
116
+ info "...commits"
117
+ begin
118
+ @client . commits_since ( repo , @date ) . each do |commit |
119
+ # if commmitter is a member of the org and not active, make active
120
+ if commit [ "author" ] . nil?
121
+ add_unrecognized_author ( commit [ :commit ] [ :author ] )
122
+ next
123
+ end
124
+ if t = @members . find { |member | member [ :login ] == commit [ "author" ] [ "login" ] && member [ :active ] == false }
125
+ make_active ( t [ :login ] )
126
+ end
127
+ end
128
+ rescue Octokit ::Conflict
129
+ info "...no commits"
130
+ end
131
+ end
132
+
133
+ def issue_activity ( repo , date = @date )
134
+ # get all issues after specified date and iterate
135
+ info "...Issues"
136
+ @client . list_issues ( repo , { :since => date } ) . each do |issue |
137
+ # if there's no user (ghost user?) then skip this // THIS NEEDS BETTER VALIDATION
138
+ if issue [ "user" ] . nil?
139
+ next
140
+ end
141
+ # if creator is a member of the org and not active, make active
142
+ if t = @members . find { |member | member [ :login ] == issue [ "user" ] [ "login" ] && member [ :active ] == false }
143
+ make_active ( t [ :login ] )
144
+ end
145
+ end
146
+ end
147
+
148
+ def issue_comment_activity ( repo , date = @date )
149
+ # get all issue comments after specified date and iterate
150
+ info "...Issue comments"
151
+ @client . issues_comments ( repo , { :since => date } ) . each do |comment |
152
+ # if there's no user (ghost user?) then skip this // THIS NEEDS BETTER VALIDATION
153
+ if comment [ "user" ] . nil?
154
+ next
155
+ end
156
+ # if commenter is a member of the org and not active, make active
157
+ if t = @members . find { |member | member [ :login ] == comment [ "user" ] [ "login" ] && member [ :active ] == false }
158
+ make_active ( t [ :login ] )
159
+ end
160
+ end
161
+ end
162
+
163
+ def pr_activity ( repo , date = @date )
164
+ # get all pull request comments comments after specified date and iterate
165
+ info "...Pull Request comments"
166
+ @client . pull_requests_comments ( repo , { :since => date } ) . each do |comment |
167
+ # if there's no user (ghost user?) then skip this // THIS NEEDS BETTER VALIDATION
168
+ if comment [ "user" ] . nil?
169
+ next
170
+ end
171
+ # if commenter is a member of the org and not active, make active
172
+ if t = @members . find { |member | member [ :login ] == comment [ "user" ] [ "login" ] && member [ :active ] == false }
173
+ make_active ( t [ :login ] )
174
+ end
175
+ end
176
+ end
177
+
178
+ def member_activity
179
+ @repos_completed = 0
180
+ # print update to terminal
181
+ info "Analyzing activity for #{ @members . length } members and #{ @repositories . length } repos for #{ @organization } \n "
182
+
183
+ # for each repo
184
+ @repositories . each do |repo |
185
+ info "rate limit remaining: #{ @client . rate_limit . remaining } "
186
+ info "analyzing #{ repo } "
187
+
188
+ commit_activity ( repo )
189
+ issue_activity ( repo )
190
+ issue_comment_activity ( repo )
191
+ pr_activity ( repo )
192
+
193
+ # print update to terminal
194
+ @repos_completed += 1
195
+ info "...#{ @repos_completed } /#{ @repositories . length } repos completed\n "
196
+ end
197
+
198
+ # open a new csv for output
199
+ CSV . open ( "inactive_users.csv" , "wb" ) do |csv |
200
+ # iterate and print inactive members
201
+ @members . each do |member |
202
+ if member [ :active ] == false
203
+ member_detail = "#{ member [ :login ] } ,#{ member [ :email ] unless member [ :email ] . nil? } "
204
+ info "#{ member_detail } is inactive\n "
205
+ csv << [ member_detail ]
206
+ end
207
+ end
208
+ end
209
+
210
+ CSV . open ( "unrecognized_authors.csv" , "wb" ) do |csv |
211
+ @unrecognized_authors . each do |author |
212
+ author_detail = "#{ author [ :name ] } ,#{ author [ :email ] } "
213
+ info "#{ author_detail } is unrecognized\n "
214
+ csv << [ author_detail ]
215
+ end
216
+ end
217
+ end
218
+ end
219
+
220
+ options = { }
221
+ OptionParser . new do |opts |
222
+ opts . banner = "#{ $0} - Find and output inactive members in an organization"
223
+
224
+ opts . on ( '-c' , '--check' , "Check connectivity and scope" ) do |c |
225
+ options [ :check ] = c
226
+ end
227
+
228
+ opts . on ( '-d' , '--date MANDATORY' , Date , "Date from which to start looking for activity" ) do |d |
229
+ options [ :date ] = d . to_s
230
+ end
231
+
232
+ opts . on ( '-e' , '--email' , "Fetch the user email (can make the script take longer" ) do |e |
233
+ options [ :email ] = e
234
+ end
235
+
236
+ opts . on ( '-o' , '--organization MANDATORY' , String , "Organization to scan for inactive users" ) do |o |
237
+ options [ :organization ] = o
238
+ end
239
+
240
+ opts . on ( '-v' , '--verbose' , "More output to STDERR" ) do |v |
241
+ @debug = true
242
+ options [ :verbose ] = v
243
+ end
244
+
245
+ opts . on ( '-h' , '--help' , "Display this help" ) do |h |
246
+ puts opts
247
+ exit 0
248
+ end
249
+ end . parse!
250
+
251
+ stack = Faraday ::RackBuilder . new do |builder |
252
+ builder . use Octokit ::Middleware ::FollowRedirects
253
+ builder . use Octokit ::Response ::RaiseError
254
+ builder . use Octokit ::Response ::FeedParser
255
+ builder . response :logger
256
+ builder . adapter Faraday . default_adapter
257
+ end
258
+
259
+ Octokit . configure do |kit |
260
+ kit . auto_paginate = true
261
+ kit . middleware = stack if @debug
262
+ end
263
+
264
+ options [ :client ] = Octokit ::Client . new
265
+
266
+ InactiveMemberSearch . new ( options )
0 commit comments