Skip to content

Commit

Permalink
copilot pruning (#31)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Abraham <[email protected]>
  • Loading branch information
itayd and daabr authored Aug 2, 2024
1 parent db6bb13 commit cc914d5
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 0 deletions.
10 changes: 10 additions & 0 deletions github_copilot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# GitHub Copilot Registration Pruning

This automation searches daily for all users in a GitHub organization that are actively using Copilot.
If Copilot was not used in a preceding period, it automatically unregisters them, and then notifies them.
Users can then optionally ask for their subscription to be reinstated.

## Slack Usage

- `/ak prune-idle-copilot-seats` invokes the automation immidetly.
- `/ak find-idle-copilot-seats` just displays the potentially idle seats.
27 changes: 27 additions & 0 deletions github_copilot/autokitteh.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
version: v1

project:
name: github_copilot
vars:
- name: GITHUB_ORG
value: "yourorg"
- name: IDLE_USAGE_THRESHOLD
value: "72h"
# If set, only manage GitHub Copilot subscriptions to the specified users, separated by commas.
- name: LOGINS
value: ""
- name: LOG_CHANNEL
value: "github-copilot-registrations"
connections:
- name: mygithub
integration: github
- name: myslack
integration: slack
triggers:
- name: check_daily
schedule: "@daily"
call: triggers.star:on_schedule
- name: check_now
connection: myslack
event_type: app_mention
call: triggers.star:on_slack_app_mention
41 changes: 41 additions & 0 deletions github_copilot/helpers.star
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
load("@github", "mygithub")
load("@slack", "myslack")

def _email_to_slack_user_id(email):
resp = myslack.users_lookup_by_email(email)
return resp.user.id if resp.ok else ""

def github_username_to_slack_user_id(github_username, github_owner_org):
resp = mygithub.get_user(github_username, owner = github_owner_org)
github_user_link = "<%s|%s>" % (resp.htmlurl, github_username)

if resp.type == "Bot":
return ""

# Try to match by the email address first.
if resp.email:
slack_user_id = _email_to_slack_user_id(resp.email)
if slack_user_id:
return slack_user_id

# Otherwise, try to match by the user's full name.
if not resp.name:
return ""

gh_full_name = resp.name.lower()
for user in _slack_users():
slack_names = (user.profile.real_name.lower(), user.profile.real_name_normalized.lower())
if gh_full_name in slack_names:
return user.id

return ""

def _slack_users(cursor = ""):
resp = myslack.users_list(cursor, limit = 100)
if not resp.ok:
return []

users = resp.members
if resp.response_metadata.next_cursor:
users += _slack_users(resp.response_metadata.next_cursor)
return users
46 changes: 46 additions & 0 deletions github_copilot/msg.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "You have been removed from the Copilot program due to inactivity",
"emoji": true
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Reinstate"
},
"accessory": {
"type": "button",
"text": {
"type": "plain_text",
"text": ":repeat:",
"emoji": true
},
"value": "reinstate",
"action_id": "button-action"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "OK"
},
"accessory": {
"type": "button",
"text": {
"type": "plain_text",
"text": ":relieved:",
"emoji": true
},
"value": "ok",
"action_id": "button-action"
}
}
]
}
82 changes: 82 additions & 0 deletions github_copilot/seats.star
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
load("env", "GITHUB_ORG", "IDLE_USAGE_THRESHOLD", "LOGINS", "LOG_CHANNEL")
load("@github", "mygithub")
load("@slack", "myslack")
load("helpers.star", "github_username_to_slack_user_id")
load("msg.json", "blocks")

logins = LOGINS.split(",")
idle_usage_threshold = time.parse_duration(IDLE_USAGE_THRESHOLD)

def prune_idle_seats():
seats = find_idle_seats()
new_idle_seats = []
for seat in seats:
new_idle_seats.append(seat)

print("new idle: {}".format(seat))
start("seats.star:engage_seat", {"seat": seat})
return new_idle_seats

def _get_all_seats():
# TODO: pagination.
return mygithub.list_copilot_seats(GITHUB_ORG).seats

def find_idle_seats():
seats = _get_all_seats()

t, idle_seats = time.now(), []
for seat in seats:
if logins and (seat.assignee.login not in logins):
print("skipping %s" % seat.assignee.login)
continue

delta = t - seat.last_activity_at
is_idle = delta >= idle_usage_threshold

print("{}: {} - {} = {} {} {}".format(
seat.assignee.login,
t,
seat.last_activity_at,
delta,
">=" if is_idle else "<",
idle_usage_threshold,
))

if is_idle:
idle_seats.append(seat)

return idle_seats

def engage_seat(seat):
log = lambda msg: myslack.chat_post_message(LOG_CHANNEL, "{}: {}".format(seat.assignee.login, msg))

log("engaging")

github_login = seat.assignee.login
slack_id = github_username_to_slack_user_id(github_login, GITHUB_ORG)
if not slack_id:
print("No slack user found for github user %s" % github_login)
return

mygithub.remove_copilot_users(GITHUB_ORG, [github_login])

myslack.chat_post_message(slack_id, blocks=blocks)

s = subscribe('myslack', 'data.type == "block_actions" && data.user.id == "{}"'.format(slack_id))

say = lambda msg: myslack.chat_post_message(slack_id, msg)

value = next_event(s)["actions"][0].value

if value == 'ok':
say("Okey dokey!")
log("ok")
return

if value == 'reinstate':
log("reinstate")
mygithub.add_copilot_users(GITHUB_ORG, [github_login])
say("You have been reinstated to the Copilot program.")
return

log("wierd response: {}".format(value))
23 changes: 23 additions & 0 deletions github_copilot/triggers.star
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
load("@slack", "myslack")
load("seats.star", "prune_idle_seats", "find_idle_seats")

def on_schedule():
print(prune_idle_seats())

def on_slack_app_mention(data):
parts = data.text.split(" ")
if len(parts) < 2:
myslack.chat_post_message(data.channel, "unrecorgnized command", thread_ts=data.ts)
return

cmd = parts[1].strip()

reply = lambda msg: myslack.chat_post_message(data.channel, msg, thread_ts=data.ts)
logins = lambda seats: [seat.assignee.login for seat in seats]

if cmd == "prune-idle-copilot-seats":
seats = prune_idle_seats()
reply("engaged {} new idle seats: {}".format(len(seats), logins(seats)))
elif cmd == "find-idle-copilot-seats":
seats = find_idle_seats()
reply("found {} idle seats: {}".format(len(seats), logins(seats)))

0 comments on commit cc914d5

Please sign in to comment.