[![pipeline status](https://gitlab.com/gitlab-org/gitlab-triage/badges/master/pipeline.svg)](https://gitlab.com/gitlab-org/gitlab-triage/commits/master) # GitLab Triage Project This project contains the library and pipeline definition to enable automated triaging of issues in the [GitLab-CE Project](https://gitlab.com/gitlab-org/gitlab-ce). ## gitlab-triage gem ### Summary The `gitlab-triage` gem aims to enable project managers and maintainers to automatically triage Issues and Merge Requests in GitLab projects based on defined policies. ### What is a triage policy? Triage policies are defined on a resource level basis, resources being: - Issues - Merge Requests Each policy can declare a number of conditions that must all be satisfied before a number of actions are carried out. ### Defining a policy Policies are defined in a policy file (by default [.triage-policies.yml](.triage-policies.yml)). The format of the file is [YAML](https://en.wikipedia.org/wiki/YAML). > Note: You can use the [`--init`](#usage) option to add an example [`.triage-policies.yml` file](support/.triage-policies.example.yml) to your project Select which resource to add the policy to: - `issues` - `merge_requests` And create an array of `rules` to define your policies: For example: ```yml resource_rules: issues: rules: - name: My policy conditions: date: attribute: updated_at condition: older_than interval_type: days interval: 5 state: opened labels: - No Label limits: most_recent: 50 actions: labels: - needs attention mention: - markglenfletcher comment: | {{author}} This issue is unlabelled after 5 days. It needs attention. Please take care of this before the end of #{2.days.from_now.strftime('%Y-%m-%d')} summarize: title: Issues require labels item: | - [ ] [{{title}}]({{web_url}}) {{labels}} summary: | The following issues require labels: {{items}} Please take care of them before the end of #{7.days.from_now.strftime('%Y-%m-%d')} /label ~"needs attention" merge_requests: rules: [] ``` ### Fields A policy consists of the following fields: - [Name field](#name-field) - [Conditions field](#conditions-field) - [Limits field](#limits-field) - [Actions field](#actions-field) #### Name field The name field is used to describe the purpose of the individual policy. Example: ```yml name: Policy name ``` #### Conditions field Used to declare a condition that must be satisfied by a resource before actions will be taken. Available condition types: - [`date` condition](#date-condition) - [`milestone` condition](#milestone-condition) - [`state` condition](#state-condition) - [`upvotes` condition](#upvotes-condition) - [`labels` condition](#labels-condition) - [`forbidden_labels` condition](#forbidden-labels-condition) - [`no_additional_labels` condition](#no-additional-labels-condition) - [`author_member` condition](#author-member-condition) - [`assignee_member` condition](#assignee-member-condition) - [`ruby` condition](#ruby-condition) ##### Date condition Accepts a hash of fields. | Field | Type | Values | Required | | --------- | ---- | ---- | -------- | | `attribute` | string | `created_at`, `updated_at` | yes | | `condition` | string | `older_than`, `newer_than` | yes | | `interval_type` | string | `days`, `weeks`, `months`, `years` | yes | | `interval` | integer | integer | yes | Example: ```yml conditions: date: attribute: updated_at condition: older_than interval_type: months interval: 12 ``` ##### Milestone condition Accepts an array of strings. Each element is the name of a milestone to filter upon. > Note: **All** specified milestones must be present on the resource for the condition to be satisfied Example: ```yml conditions: milestone: - v1 - v2 ``` ##### State condition Accepts a string. | State | Type | Value | | --------- | ---- | ------ | | Closed issues | string | `closed` | | Open issues | string | `opened` | Example: ```yml conditions: state: opened ``` ##### Upvotes condition Accepts a hash of fields. | Field | Type | Values | Required | | --------- | ---- | ---- | -------- | | `attribute` | string | `upvotes`, `downvotes` | yes | | `condition` | string | `less_than`, `greater_than` | yes | | `threshold` | integer | integer | yes | Example: ```yml conditions: upvotes: attribute: upvotes condition: less_than threshold: 10 ``` ##### Labels condition Accepts an array of strings. Each element in the array represents the name of a label to filter on. > Note: **All** specified labels must be present on the resource for the condition to be satisfied Example: ```yml conditions: labels: - feature proposal ``` ###### Labels over sequences The name of a label can contain one or more sequence conditions, written like `{0..9}`, which means `0`, `1`, `2`, and so on up to `9`. For each number, the rule will be duplicated with the new label name. Example: ```yml resource_rules: issues: rules: - name: Add missing ~"missed\-deliverable" label conditions: labels: - missed:{10..11}.{0..1} - deliverable actions: labels: - missed deliverable ``` Which will be expanded into: ```yml resource_rules: issues: rules: - name: Add missing ~"missed\-deliverable" label conditions: labels: - missed:10.0 - deliverable actions: labels: - missed deliverable - name: Add missing ~"missed\-deliverable" label conditions: labels: - missed:10.1 - deliverable actions: labels: - missed deliverable - name: Add missing ~"missed\-deliverable" label conditions: labels: - missed:11.0 - deliverable actions: labels: - missed deliverable - name: Add missing ~"missed\-deliverable" label conditions: labels: - missed:11.1 - deliverable actions: labels: - missed deliverable ``` ##### Forbidden labels condition Accepts an array of strings. Each element in the array represents the name of a label to filter on. > Note: **All** specified labels must be absent on the resource for the condition to be satisfied Example: ```yml conditions: forbidden_labels: - awaiting feedback ``` ##### No additional labels condition Accepts a boolean. If `true` the resource cannot have more labels than those specified by the `labels` condition. Example: ```yml conditions: labels: - feature proposal no_additional_labels: true ``` ##### Author Member condition This condition determines whether the author of a resource is a member of the specified group or project. This is useful for determining whether Issues or Merge Requests have been raised by a Community Contributor. Accepts a hash of fields. | Field | Type | Values | Required | | --------- | ---- | ---- | -------- | | `source` | string | `group`, `project` | yes | | `condition` | string | `member_of`, `not_member_of` | yes | | `source_id` | integer or string | gitlab-org/gitlab-ce | yes | Example: ```yml conditions: author_member: source: group condition: not_member_of source_id: 9970 ``` ##### Assignee Member condition This condition determines whether the assignee of a resource is a member of the specified group or project. Accepts a hash of fields. | Field | Type | Values | Required | | --------- | ---- | ---- | -------- | | `source` | string | `group`, `project` | yes | | `condition` | string | `member_of`, `not_member_of` | yes | | `source_id` | integer or string | gitlab-org/gitlab-ce | yes | Example: ```yml conditions: assignee_member: source: group condition: not_member_of source_id: 9970 ``` ##### Ruby condition This condition allows users to write a Ruby expression to be evaluated for each resource. If it evaluates to a truthy value, it satisfies the condition. If it evaluates to a falsey value, it does not satisfy the condition. Accepts a string as the Ruby expression. Example: ```yml conditions: ruby: Date.today > milestone.succ.start_date ``` In the above example, this describes that we want to act on the resources which passed the next active milestone's starting date. Here `milestone` will return a `Gitlab::Triage::Resource::Milestone` object, representing the milestone of the questioning resource. `Milestone#succ` would return the next active milestone, based on the `start_date` of all milestones along with the representing milestone. If the milestone was coming from a project, then it's based on all active milestones in that project. If the milestone was coming from a group, then it's based on all active milestones in the group. If we also want to handle some edge cases, for example, a resource might not have a milestone, and a milestone might not be active, and there might not have a next milestone. We could instead write something like: ```yml conditions: ruby: milestone&.active? && milestone&.succ && Date.today > milestone.succ.start_date ``` This will make it only act on resources which have active milestones and there exists next milestone which has already started. See [Ruby expression API](#ruby-expression-api) for the list of currently available API. #### Limits field Limits restrict the number of resources on which an action is carried out. They can be useful when combined with conditions that return a large number of resources. For example, if the conditions are satisfied by thousands of issues a limit can be configured to process only fifty of them to avoid making an overwhelming number of changes at once. Accepts a key and value pair where the key is `most_recent` or `oldest`and the value is the number of resources to act on. The following table outlines how each key affects the sorting and order of resources that it limits. | Name / Key | Sorted by | Order | | --------- | ---- | ------ | | `most_recent` | `created_at` | descending | | `oldest` | `created_at` | ascending | Example: ```yml limits: most_recent: 50 ``` #### Actions field Used to declare an action to be carried out on a resource if **all** conditions are satisfied. Available action types: - [`labels` action](#labels-action) - [`remove_labels` action](#remove-labels-action) - [`status` action](#status-action) - [`mention` action](#mention-action) - [`comment` action](#comment-action) - [`summarize` action](#summarize-action) ##### Labels action Adds a number of labels to the resource. Accepts an array of strings. Each element is the name of a label to add. Example: ```yml actions: labels: - feature proposal - awaiting feedback ``` ##### Remove labels action Removes a number of labels from the resource. Accepts an array of strings. Each element is the name of a label to remove. Example: ```yml actions: remove_labels: - feature proposal - awaiting feedback ``` ##### Status action Changes the status of the resource. Accepts a string. | State transition | Type | Value | | --------- | ---- | ------ | | Close the resource | string | `close` | | Reopen the resource | string | `reopen` | Example: ```yml actions: status: close ``` ##### Mention action Mentions a number of users. Accepts an array of strings. Each element is the username of a user to mention. Example: ```yml actions: mention: - rymai - markglenfletcher ``` ##### Comment action Adds a comment to the resource. Accepts a string, and placeholders. Placeholders should be wrapped in double curly braces, e.g. `{{author}}`. The following placeholders are supported: - `created_at`: the resource's creation date - `updated_at`: the resource's last update date - `closed_at`: the resource's closed date (if applicable) - `merged_at`: the resource's merged date (if applicable) - `state`: the resources's current state: `opened`, `closed`, `merged` - `author`: the username of the resource's author as `@user1` - `assignee`: the username of the resource's assignee as `@user1` - `assignees`: the usernames of the resource's assignees as `@user1, @user2` - `closed_by`: the user that closed the resource as `@user1` (if applicable) - `merged_by`: the user that merged the resource as `@user1` (if applicable) - `milestone`: the resource's current milestone - `labels`: the resource's labels as `~label1, ~label2` - `upvotes`: the resources's upvotes count - `downvotes`: the resources's downvotes count - `title`: the resource's title - `web_url`: the web URL pointing to the resource If the resource doesn't respond to the placeholder, or if the field is `nil`, the placeholder is not replaced. Example without placeholders: ```yml actions: comment: | Closing this issue automatically ``` Example with placeholders: ```yml actions: comment: | {{author}} Are you still interested in finishing this merge request? ``` ###### Ruby expression The comment can also contain Ruby expression, using Ruby's own string interpolation syntax: `#{ expression }`. This gives you the most flexibility. Suppose you want to mention the next active milestone relative to the one associated with the resource, you can write: ```yml actions: comment: | Please move this to %"#{milestone.succ.title}". ``` See [Ruby expression API](#ruby-expression-api) for the list of currently available API. **Note:** If you get a syntax error due to stray braces (`{` or `}`), use `\` to escape it. For example: ```yml actions: comment: | If \} comes first and/or following \{, you'll need to escape them. If it's just { wrapping something } then you don't need to, but it's also fine to escape them like \{ this \} if you prefer. ``` ##### Summarize action Generates an issue summarizing what was triaged. Accepts a hash of fields. | Field | Type | Description | Required | Placeholders | Ruby expression | | ---- | ---- | ---- | ---- | ---- | ---- | | `title` | string | The title of the generated issue | yes | no | no | | `item` | string | Template representing each triaged resource | no | yes | yes | | `summary` | string | The description of the generated issue | no | Only `{{items}}` and `{{title}}` | yes | **Note:**: Both `item` and `summary` fields act like a [comment action](#comment-action), therefore [Ruby expression](#ruby-expression) is supported. Placeholders work regularly for `item`, but for `summary` only `{{items}}` and `{{title}}` are supported because it's not tied to a particular resource like the comment action. The following placeholders are supported for `summary`: - `items`: Concatenated markdown separated by a newline for each `item` - `title`: The title of the generated issue Example: ```yml limits: most_recent: 15 actions: summarize: title: Issues require labels item: | - [ ] [{{title}}]({{web_url}}) {{labels}} summary: | The following issues require labels: {{items}} Please take care of them before the end of #{7.days.from_now.strftime('%Y-%m-%d')} /label ~"needs attention" ``` Which could generate an issue like: * Title: ``` Issues require labels ``` * Description: ``` markdown The following issues require labels: - [ ] [An example issue](http://example.com/group/project/issues/1) ~"label A", ~"label B" - [ ] [Another issue](http://example.com/group/project/issues/2) ~"label B", ~"label C" Please take care of them before the end of 2000-01-01 /label ~"needs attention" ``` ### Ruby expression API Here's a list of currently available Ruby expression API: ##### API | Name | Return type | Description | | ---- | ---- | ---- | | resource | Hash | The hash containing the raw data of the resource | | milestone | Milestone | The milestone attached to the resource | ##### Methods for `Milestone` | Method | Return type | Description | | ---- | ---- | ---- | | id | Integer | The id of the milestone | | iid | Integer | The iid of the milestone | | project_id | Integer | The project id of the milestone if available | | group_id | Integer | The group id of the milestone if available | | title | String | The title of the milestone | | description | String | The description of the milestone | | state | String | The state of the milestone. Could be `active` or `closed` | | due_date | Date | The due date of the milestone. Could be `nil` | | start_date | Date | The start date of the milestone. Could be `nil` | | updated_at | Time | The updated timestamp of the milestone | | created_at | Time | The created timestamp of the milestone | | succ | Milestone | The next active milestone beside this milestone | | active? | Boolean | `true` if `state` is `active`; `false` otherwise | ### Usage ``` Usage: gitlab-triage [options] -n, --dry-run Don't actually update anything, just print -f, --policies-file [string] A valid policies YML file -p, --project-id [string] A project ID or path -t, --token [string] A valid API token -H, --host-url [string] A valid host url -d, --debug Print debug information -h, --help Print help message --init Initialize the project with a policy file --init-ci Initialize the project with a .gitlab-ci.yml file ``` #### Local ``` gem install gitlab-triage gitlab-triage --help gitlab-triage --dry-run --token $API_TOKEN --project-id gitlab-org/triage ``` #### GitLab CI pipeline You can enforce policies using a scheduled pipeline: ```yml run:triage:triage: stage: triage script: - gem install gitlab-triage - gitlab-triage --token $API_TOKEN --project-id $CI_PROJECT_PATH only: - schedules ``` > Note: You can use the [`--init-ci`](#usage) option to add an example [`.gitlab-ci.yml` file](support/.gitlab-ci.example.yml) to your project #### Can I use gitlab-triage for my self-hosted GitLab instance? Yes, you can override the host url using the following options: ##### CLI ``` gitlab-triage --dry-run --token $API_TOKEN --project-id gitlab-org/triage --host-url https://gitlab.host.com ``` ##### Policy file ```yml host_url: https://gitlab.host.com resource_rules: ``` ### Contributing Please refer to the [Contributing Guide](CONTRIBUTING.md)