From db7cfb632ec9c6dd838fa2d832e116f51c4fd1ed Mon Sep 17 00:00:00 2001 From: Johannes Tandler Date: Fri, 26 Apr 2024 10:10:08 +0200 Subject: [PATCH 1/4] Documentation updates --- README.md | 213 +++++++++++++++++++++++---------------- ic-assignment/action.yml | 19 ++-- 2 files changed, 133 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 7b629c0..d910abe 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,105 @@ -# GitHub action to auto-assign issues to team members based on their availability and busyness +# GitHub Action(s) to help automatically assign issues to the right persons -This repository contains 2 actions which may be useful to someone assigning issues to dedicated engineering teams and distributing (automatically assigning) them based on the availability of individual engineers. +This repository contains 2 actions which are useful to someone assigning issues to dedicated engineering teams and distributing (automatically assigning) them based on the availability of individual engineers: ## Regex labeler -This github action allows to assign labels based on regular expressions which are ranked against the issue title and body it's run against. This allows to assign team specific labels by matching Issues against these regex's. +This action allows to assign labels based on regular expressions which are ranked against the issue title and body it's run against. This allows to assign team specific labels by matching issues against these regex's. + +### Inputs + +| Parameter | Type | Required | Default | Description | +| ---------- | ------ | -------- | --------------------------- | ------------------------------------------ | +| `gh-token` | String | false | `${{ github.token }}` | The github token to be used for API access | +| `cfg-path` | String | true | `.github/regex-labeler.yml` | Path for regex labeler config file | + +### Configuration + +An exemplary configuration looks like this: + +```yaml +required_labels: +labels: + regex-labeler: + matchers: + - regex: "regex" + ic-assignment: + matchers: + - regex: "assignment" + weight: 10 +``` + +In this example the action would apply to all new issues (as no `required_labels` are set). If the issue title and/or body contains the word "regex" it would be labeled as `regex-labeler` by default. But if the word `assignment` is matched, the label `ic-assignment` would be assigned instead (given the higher weight). + +#### Configuration structs + +Root configuration struct: + +| Parameter | Type | Required | Default | Description | +| ----------------- | --------------------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `required_labels` | List of Strings | false | `[]` | List of labels which are required to run this action. If it's triggered on an issue which doesn't have **all** required labels, it exits without doing something | +| `labels` | Map op label configurations | true | `nil` | Definition of which labels are assigned by which matcher configuration. Only the label with the best matching matchers is assigned | + +Matcher configuration: + +| Parameter | Type | Required | Default | Description | +| ---------- | --------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `matchers` | List of Matcher | false | `[]` | List matchers which are required to assign this label. Each matching matchers increases the likeliness (by weight) the owning label is assigned. At least one matcher needs to match to assign a label. | + +Matcher: + +| Parameter | Type | Required | Default | Description | +| --------- | ------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `regex` | String | true | `` | The regular expression a label issue and body are checked against. If there is any match, this matcher is seen as successful | +| `weight` | Integer | false | `1` | The weight of this matcher. Can be used to overwrite other label's matcher. E.g. A weight of 10, would overrule another label with 9 individual matchers matching (if they have the default weight) | + -TODO: Document configuration and usage ## IC-Assignment -This action allows to distribute new issues among individuals based on their load and availability. +This action assigns individual members of teams to an incoming issue. First the matching team is determined by a set of labels required by a given team. After a team has been matched, it tries to assign the issue to the member of a team who is available and least busy (in comparison to the rest of their team). If multiple members of a team are seen as available and have the same lowest level of busyness, the issue is assigned randomly to one of them. In case no one is found who is available, the action will still assign it to someone in the team (chosen randomly) to ensure no issue is lost. + +### Way of working + +#### Determine availability of individual members of a team + +Availability of individual members of a team is determined by accessing their calendar and checking for single events which mark them as busy for more than **6hrs** (not configurable at the moment) and is still ongoing at the time the issue is created or is about to start in the next 12-48hrs. The lookahead time differs as the process tries to accomodate weekends when usually no one is working. On a Friday the upcoming Monday is therefore checked for potential events of this kind. + +Calendars are accessed either via a public ical feed or via google calendar api (see configuration below for more details). + +#### Determine busyness of individual members of a team + +Busyness of team members is calculated by the amount of issues someone is assigned to and which got updated in the past 5 days (not configurable at the moment). Issues updated in this timeframe are taken into account if +* Open issues: If they don't contain any of the labels which are configured to be ignored. This allows for example to ignore issues which got marked as `stale` +* Closed issues: If they were closed within the lookback time (5 days). + +The higher this count, the more busy an individual team member is seen compared to other members. + +### Inputs -### Basic process +| Parameter | Type | Required | Default | Description | +| ------------------------- | ------- | -------- | ----------------------------- | -------------------------------------------------------------------------------------------------------- | +| `gh-token` | String | false | `${{ github.token }}` | The github token to be used for API access | +| `cfg-path` | String | true | `.auto-assign-issue-cfg.yaml` | Path to configuration file which contains definition of teams and requirements to match them on an issue | +| `dry-run` | Boolean | false | `false` | If set to true, assignment will only be logged. | +| `gcal-service-acount-key` | String | false | `` | If set, this service account key will be used to check availability for google calendars. | -* Teams are matched by given label -* Availability is checked by fetching the ical or google calendar availability per person. A person is not seen as available if they have busy events scheduled for periods longer than 4hr the given business day. -* Load is determined by checking if team members already have issues assigned to them which had activity in the past 5 business days (except `ignoreLables` marks an issue as ignored). +### Outputs +| Parameter | Type | Default | Description | +| ---------- | ------ | ------- | ---------------------------------------------------------------------------------------------------- | +| `assignee` | String | `name` | Name of the member assigned to the issue. If custom `output` is defined it's used instead of `name`. | -### Team configuration +### Configuration -The configuration file is fetched from the repository this action runs in based on the `cfg-path` input parameter. `repo-token` is used to fetch this file, hence read access to the repo is required. In the Go version of the escalation scheduler `repo-token` has been renamed to `gh-token`. +The configuration file is fetched from the repository this action runs in based on the `cfg-path` input parameter. `gh-token` is used to fetch this file, hence read access to the repo is required. Example config: ```yaml teams: team_a: - requireLabel: "product_a" + requireLabel: + - "product_a" members: - name: user1 ical-url: https://.../cal.ics @@ -40,8 +112,6 @@ teams: requireLabel: - "product_b" - "product_b-important" - matchers: - - "^IMPORTANT:" members: - name: manager output: "slack-handle" @@ -49,95 +119,60 @@ ignoreLabels: - stale ``` -### Find matching team - -Teams are taken from the configuration file. To match an issue against a team the following criteria have to be true: -* `requireLabel` will verify that the respective label is set on the issue. This can be a string, comma-seperated-list or array. In case of multiple labels specified, it's only checked that one of the labels match. -* `matchers` can be a list of regex expression. If defined, the team with the most matches will get the assignment. If a team has no `matchers` it will always be superseded by teams with a matcher. - -If no team is matched, the action will exit here. - -### Configuration settings per team member - -Within the configuration file you can specify various options per team member: -* `name`: The Github handle of the individual team member -* `ical-url`: The URL of the ical used for availability checking. Needs to be publicly accessible for now. -* `output`: Allows to define an individual output value set as `assignee` ouput in case this team member is assigned. For example this may be used to define a slack handle which is then passed to the next action in order to notify someone on slack. - -#### How to add yourself to a team - -Assuming our configuration file looks like this: -```yaml -teams: - team_a: - requireLabel: "product_a" - members: - - name: user1 - ical-url: JOHN_DOE_ICAL_URL -``` - -#### Add your ical calendar - -The `ical-url` can be used to configure arbitrary ical urls to be used for availability checking. This URL needs to be publicly accessible without any authorization. As this is a less secure and less performant way of checking availability, it is recommended to use the Google calendar integration if possible. - -#### Add your gcal calendar - -This action works by using a google cloud service account to access the calendar of team members. Therefore it's required first to setup the necessary service account with access to the google calendar api. - -Next members can share their availability with this service account via: -1. Open [Google Calendar](https://calendar.google.com/calendar/u/0/r) -2. Open Settings by opening the hamburger menu next to your personal calendar and clicking `Settings and sharing` -3. In the `Share with specific people or groups` section you click `+ Add people and groups` -4. Enter the email address of the service account and select `See only free/busy (hide details)` under Permissions. -5. Click `Send` - -You are done. +#### Root configuration struct -### Calculate availability of team members +| Parameter | Type | Required | Default | Description | +| -------------- | -------------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ignoreLabels` | List of Strings | false | `[]` | List of labels which mark this issue to be ignored. If triggered on an issue which has **one** of the labels to be ignored, the action exits without doing something | +| `teams` | Map of Team configurations | true | `nil` | Definition of the teams this issue is distributed between. | -`members` of each team defines their respective members as a tuple of github name and optionally a ical calender to determine availability. If an ical calender is defined, the team member will be **marked as unavailable if an event exists (which marks them busy) for the day of the run which lasts longer than 4hrs**. If no ical is defined, team members are always seen as available. +#### Team configuration struct -* members can be part of multiple teams +| Parameter | Type | Required | Default | Description | +| -------------- | --------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `requireLabel` | List of Strings | false | `[]` | List of labels which are required to match a given team. Only if all labels match, the issue may be assigned to someone of this team. If multiple teams match all members of all matching teams are considered. | +| `members` | List of Members | true | `nil` | Definition of the individual members of a team. | -### Filter all team members which are already busy with other escalations -Additionally team members who already have a relevant issue which matches the team criteria (`requireLabel`) assigned to them are filtered out. +#### Member configuration struct -An issue is relevant if it satisfies the following conditions: +| Parameter | Type | Required | Default | Description | +| ---------------- | ------ | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | String | true | `` | Github handle of a team member | +| `output` | String | false | `` | Value which is set as output of this action in case this member is assigned. E.g. can be used with slack handles to map users to their slack names and notify them in a later step in the workflow. If not specified `name` is used instead. | +| `ical-url` | String | false | `` | Public ICal feed of this member used to determine availability of someone at a given time. | +| `googleCalendar` | String | false | `` | Google Calendar name which is checked through the specified service account to determine availability. If set, `ical-url` is ignored. | -- If it is open - - It was updated within the "lookBackPeriod" - - It does not have a label listed in "ignoreLabels" (for example "stale") -- If it is closed - - It was closed within the "lookBackPeriod" +### Considerations -The "lookBackPeriod" is 5 days, except if it is includes a weekend day, in which case it is 7 days. +#### Timezone awareness -### Choose available team member - -One of the remaining team members is randomly chosen (to be improved to fairly distribute issues between all members). +* Not timezone aware - The intent is to distribute issues fairly between everybody, therefore we made the decision to not respect timezones. If you are interested in fixing this please open an issue and explain your usecase there. -### Assign +#### Fairness +* Not completely fair - Some team members might resolve issues quicker than others and will potentially end up with more issues in the long run. Adding state to a github action to track the amount of completed issues is hard and people who get issues which are very hard to resolve (and therefore take a long time) would suffer from this. We will revisit the current approach after gaining experience with it, but start lightweight now. -Final step is to assign the issue to the actual member. If `dryRun` is set this step is only logged. +### Google calendar configuration -### Inputs +If you want to use the functionality to determine availability based on someones `googleCalendar` a service account with google calendar api access is needed. To create this: -| Parameter | Type | Required | Default | Description | -| --------------------------- | ------- | -------- | --------------------------- | -------------------------------------------------------------------------------------------------------- | -| `repo-token` | String | true | $GITHUB_TOKEN | Token to be used for all github operations. | -| `cfg-path` | String | true | .auto-assign-issue-cfg.yaml | Path to configuration file which contains definition of teams and requirements to match them on an issue | -| `abort-if-already-assigned` | Boolean | false | false | Abort if issue already has assignee. | -| `dry-run` | Boolean | false | false | If set to true, assignment will only be logged. | -| `gcal_service_acount_key` | String | false | "" | If set, this service account key will be used to check availability for google calendars. | +#### Service account creation -### Outputs +1. Open [Google cloud console](https://console.cloud.google.com/) +2. Choose a project this fits in / works well or create a new one as described [here](https://developers.google.com/workspace/guides/create-project) +3. Activate Google Calendar API for your project [here](https://console.cloud.google.com/flows/enableapi?apiid=calendar-json.googleapis.com) +4. Create a service account for your project as described [here](https://cloud.google.com/iam/docs/service-accounts-create) +5. Open the service account from the [service account overview](https://console.cloud.google.com/iam-admin/serviceaccounts) +6. **Remember the service account email**. This is the email all configured members have to share their calendar with (free/busy information is enough) +7. Go to **Keys** > `ADD KEY` and create a new json based key which is used as `gcal-service-acount-key` during workflow runs. It's recommended to store as a secret in your repo and use the secret during workflow runs as input. -| Parameter | Type | Default | Description | -| ---------- | ------ | ------- | ---------------------------------------------------------------------------------------------------- | -| `assignee` | String | `name` | Name of the member assigned to the issue. If custom `output` is defined it's used instead of `name`. | +#### Share calendars with the service account -### Current limitations +This action works by using a google cloud service account to access the calendar of team members. Therefore it's required first to setup the necessary service account with access to the google calendar api. -* Not timezone aware - The intent is to distribute issues fairly between everybody, therefore we made the decision to not respect timezones. If you are interested in fixing this please open an issue and explain your usecase there. -* Not completely fair - Some team members might resolve issues quicker than others and will potentially end up with more issues in the long run. Adding state to a github action to track the amount of completed issues is hard and people who get issues which are very hard to resolve (and therefore take a long time) would suffer from this. We will revisit the current approach after gaining experience with it, but start lightweight now. +Next members can share their availability with this service account via: +1. Open [Google Calendar](https://calendar.google.com/calendar/u/0/r) +2. Open Settings by opening the hamburger menu next to your personal calendar and clicking `Settings and sharing` +3. In the `Share with specific people or groups` section you click `+ Add people and groups` +4. Enter the email address of the service account you created and select `See only free/busy (hide details)` under Permissions. +5. Click `Send` and you are done \ No newline at end of file diff --git a/ic-assignment/action.yml b/ic-assignment/action.yml index a0ae790..dd86558 100644 --- a/ic-assignment/action.yml +++ b/ic-assignment/action.yml @@ -1,25 +1,24 @@ name: "ic-assignment" -description: "Finds a person to assign a support escalation to and assigns it" +description: "Finds a person who is available (based on their calendar) and is least busy (based on assigned issues) and assigns an incoming issue to that person" inputs: gh-token: - description: "The GITHUB_TOKEN, needed to update the issue" + description: "The GITHUB_TOKEN, which is used to check busyness of team members (therefore requires read access) and assigns the issue (requires write access)" default: ${{ github.token }} cfg-path: - description: "path to cfg yaml file which defines which teams (and their members) exist" + description: "path to cfg yml file which defines which teams (and their members) exist" required: true - default: .auto-assign-issue-cfg.yaml - abort-if-already-assigned: - description: "Abort the action if somebody is already assigned." - required: false - default: "true" + default: .auto-ic-assignment-cfg.yml dry-run: - description: "Only log what this action would do, but don't assign the issue in the end." + description: "With this option set only logs, but doesn't assign the issue in the end." required: false default: "true" - gcal_service_acount_key: + gcal-service-acount-key: description: "Used to access google calendars in case of being configured for team members" required: false default: "" +outputs: + assignee: + description: 'The output property of the assigned person. If output property is empty, name is used instead' runs: using: "docker" image: "docker://ghcr.io/grafana/issue-team-scheduler-ic-assignment:v0.5" From cbf9a38e2c592db86aa05f0996e61813f77c5197 Mon Sep 17 00:00:00 2001 From: Johannes Tandler Date: Fri, 26 Apr 2024 10:30:01 +0200 Subject: [PATCH 2/4] Minor updates to considerations --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d910abe..dcd560e 100644 --- a/README.md +++ b/README.md @@ -147,10 +147,11 @@ ignoreLabels: #### Timezone awareness -* Not timezone aware - The intent is to distribute issues fairly between everybody, therefore we made the decision to not respect timezones. If you are interested in fixing this please open an issue and explain your usecase there. +During the initial implementation it was carefully discussed if the action should take timzeones into consideration (aka only assign issues to someone during their working hours). After in-depth analysis of issues in other projects we concluded that the creation time of issues don't align to working hours of the majority of the teams we were working in (e.g. majority of issues created during american working hours while majority of the team wasn't located there). **This led us to make the action not timezone aware as of today.** As this obviously cause issues when you need issues to handle in a timely manner we are open to proposals for improvement, if they take into account that our main priority is to distribute issues as fairly as possible across a team. #### Fairness -* Not completely fair - Some team members might resolve issues quicker than others and will potentially end up with more issues in the long run. Adding state to a github action to track the amount of completed issues is hard and people who get issues which are very hard to resolve (and therefore take a long time) would suffer from this. We will revisit the current approach after gaining experience with it, but start lightweight now. + +Fairness in assigning issues to individuals of a team is subjective. As mean time to resolve depends on time of creation, issue complexity, productivity, knowledge, experience, availability and some degree of luck per team member it needs to be acknowledged that their is no objectional fair distribution of issues among a group of people. Taking all these metrics into account is almost impossible and most likely would require additional state which we would need to store somewhere. We also discussed ideas like the tracking the amount of completed issues per IC, but had to realise that issues which were hard to resolve (and therefore take a long time) would suffer from this. We will revisit the current approach after gaining experience with it, but start lightweight now. ### Google calendar configuration From 81d506890ee71a0e02537a9acf5c551125e978d5 Mon Sep 17 00:00:00 2001 From: Johannes Tandler Date: Fri, 26 Apr 2024 13:57:46 +0200 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: Mauro Stettler --- README.md | 6 +++--- ic-assignment/action.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index dcd560e..6c8e01b 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Matcher configuration: | Parameter | Type | Required | Default | Description | | ---------- | --------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `matchers` | List of Matcher | false | `[]` | List matchers which are required to assign this label. Each matching matchers increases the likeliness (by weight) the owning label is assigned. At least one matcher needs to match to assign a label. | +| `matchers` | List of Matcher | false | `[]` | List matchers which are required to assign this label. Each matching matcher increases the likeliness (by weight) the owning label is assigned. At least one matcher needs to match to assign a label. | Matcher: @@ -123,7 +123,7 @@ ignoreLabels: | Parameter | Type | Required | Default | Description | | -------------- | -------------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ignoreLabels` | List of Strings | false | `[]` | List of labels which mark this issue to be ignored. If triggered on an issue which has **one** of the labels to be ignored, the action exits without doing something | +| `ignoreLabels` | List of Strings | false | `[]` | List of labels which mark this issue to be ignored. If triggered on an issue which has at least **one** of the labels to be ignored, the action exits without doing something | | `teams` | Map of Team configurations | true | `nil` | Definition of the teams this issue is distributed between. | #### Team configuration struct @@ -151,7 +151,7 @@ During the initial implementation it was carefully discussed if the action shoul #### Fairness -Fairness in assigning issues to individuals of a team is subjective. As mean time to resolve depends on time of creation, issue complexity, productivity, knowledge, experience, availability and some degree of luck per team member it needs to be acknowledged that their is no objectional fair distribution of issues among a group of people. Taking all these metrics into account is almost impossible and most likely would require additional state which we would need to store somewhere. We also discussed ideas like the tracking the amount of completed issues per IC, but had to realise that issues which were hard to resolve (and therefore take a long time) would suffer from this. We will revisit the current approach after gaining experience with it, but start lightweight now. +Fairness in assigning issues to individuals of a team is subjective. As mean time to resolve depends on time of creation, issue complexity, productivity, knowledge, experience, availability and some degree of luck per team member it needs to be acknowledged that their is no objective fair distribution of issues among a group of people. Taking all these metrics into account is almost impossible and most likely would require additional state which we would need to store somewhere. We also discussed ideas like tracking the amount of completed issues per IC, but had to realise that issues which were hard to resolve (and therefore take a long time) would suffer from this. We will revisit the current approach after gaining experience with it, but start lightweight now. ### Google calendar configuration diff --git a/ic-assignment/action.yml b/ic-assignment/action.yml index dd86558..0e34d00 100644 --- a/ic-assignment/action.yml +++ b/ic-assignment/action.yml @@ -9,7 +9,7 @@ inputs: required: true default: .auto-ic-assignment-cfg.yml dry-run: - description: "With this option set only logs, but doesn't assign the issue in the end." + description: "With this option the decision only gets logged, but not change is made to the issue." required: false default: "true" gcal-service-acount-key: From 148ff2715f07bee059c2eaaba25b01080d34bcb1 Mon Sep 17 00:00:00 2001 From: Johannes Tandler Date: Fri, 26 Apr 2024 14:08:05 +0200 Subject: [PATCH 4/4] Address pr feedback --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6c8e01b..1f57245 100644 --- a/README.md +++ b/README.md @@ -20,16 +20,16 @@ An exemplary configuration looks like this: ```yaml required_labels: labels: - regex-labeler: + potential-bug: matchers: - - regex: "regex" - ic-assignment: + - regex: "(bug|not working)" + performance-degradation: matchers: - - regex: "assignment" + - regex: "(slower|increased.*consumption)" weight: 10 ``` -In this example the action would apply to all new issues (as no `required_labels` are set). If the issue title and/or body contains the word "regex" it would be labeled as `regex-labeler` by default. But if the word `assignment` is matched, the label `ic-assignment` would be assigned instead (given the higher weight). +In this example the action would apply to all new issues (as no `required_labels` are set). If the issue title and/or body contains the words "bug" or "not working" it would be labeled as `potential-bug` by default. But if the words `slower` or a combination of `increased` and `consumption` is matched, the label `performance-degradation` would be assigned instead (given the higher weight). #### Configuration structs