GitLab triage automation project.
Homepage Repository Rubygems Documentation Download
gem install gitlab-triage -v 1.42.2
This project allows to automate triaging of issues and merge requests for GitLab projects or groups.
The gitlab-triage
gem aims to enable project managers and maintainers to
automatically triage Issues and Merge Requests in GitLab projects or groups
based on defined policies.
See Running locally for how to specify a project or a group.
Triage policies are defined on a resource level basis, resources being:
Each policy can declare a number of conditions that must all be satisfied before a number of actions are carried out.
Summary policies are special policies that join multiple policies together to create a summary issue with all the sub-policies' summaries, see Summary policies.
Policies are defined in a policy file (by default ./.triage-policies.yml
).
The format of the file is YAML.
Note: You can use the
--init
option to add an example.triage-policies.yml
file 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:
resource_rules:
issues:
rules:
- name: My policy
conditions:
date:
attribute: updated_at
condition: older_than
interval_type: days
interval: 5
state: opened
labels:
- None
limits:
most_recent: 50
actions:
labels:
- needs attention
mention:
- markglenfletcher
move: gitlab-org/backlog
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:
destination: gitlab-org/gitlab-triage
title: |
#{resource[:type].capitalize} require labels
item: |
- [ ] [{{title}}]({{web_url}}) {{labels}}
redact_confidential_resources: false
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:
- name: My policy
conditions:
state: opened
labels:
- None
limits:
most_recent: 50
actions:
labels:
- needs attention
comment_type: thread
comment: |
{{author}} This issue is unlabelled. Please add one or more labels.
We're enforcing multiple polices with pipeline schedules at triage-ops, where we're also extensively utilizing the plugins system.
A policy consists of the following fields:
The name field is used to describe the purpose of the individual policy.
Example:
name: Policy name
Used to declare a condition that must be satisfied by a resource before actions will be taken.
Available condition types:
date
conditionmilestone
conditionstate
conditionupvotes
conditionlabels
conditionforbidden_labels
conditionno_additional_labels
conditionauthor_member
conditionassignee_member
conditionsource_branch
conditiontarget_branch
conditionruby
conditionAccepts a hash of fields.
Field | Type | Values | Required |
---|---|---|---|
attribute |
string |
created_at , updated_at , merged_at
|
yes |
condition |
string |
older_than , newer_than
|
yes |
interval_type |
string |
days , weeks , months , years
|
yes |
interval |
integer | integer | yes |
Note:
merged_at
only works on merge requests.
Example:
conditions:
date:
attribute: updated_at
condition: older_than
interval_type: months
interval: 12
Accepts the name of a milestone to filter upon.
Example:
conditions:
milestone: v1
Accepts a string.
State | Type | Value |
---|---|---|
Closed issues | string | closed |
Open issues | string | opened |
Merged merge requests | string | merged |
Example:
conditions:
state: opened
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:
conditions:
upvotes:
attribute: upvotes
condition: less_than
threshold: 10
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:
conditions:
labels:
- feature proposal
Basing on the issues API, there are two special predefined label names we can use here:
None
: This indicates that no labels were presentAny
: This indicates that any labels were presentedExample:
conditions:
labels:
- None
We could expand the labels by using brace expansion, which is a pattern
surrounded by using braces: {}
. For now, we support 2 kinds of brace
expansion:
{ apple, orange }
{1..4}
Note:
- Spaces around the items are ignored.
- Do not rely on the expansion ordering. This is subject to change.
The name of a label can contain a list of items, written like
{ apple, orange }
. For each item, the rule will be duplicated with the new
label name.
Example:
resource_rules:
issues:
rules:
- name: Add missing ~Quality label
conditions:
labels:
- Quality:test-{ gap, infra }
actions:
labels:
- Quality
Which will be expanded into:
resource_rules:
issues:
rules:
- name: Add missing ~Quality label
conditions:
labels:
- Quality:test-gap
actions:
labels:
- Quality
- name: Add missing ~Quality label
conditions:
labels:
- Quality:test-infra
actions:
labels:
- Quality
Note: If you want to define a full label expansion, you'll need to force string or quote string because otherwise it won't be considered a string due to the YAML parser. For example, we can quote the expression like
'{ apple, orange }'
, which will create 2 rules, for the two specified labels.
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:
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:
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
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:
conditions:
forbidden_labels:
- awaiting feedback
Accepts a boolean. If true
the resource cannot have more labels than those specified by the labels
condition.
Example:
conditions:
labels:
- feature proposal
no_additional_labels: true
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 | yes |
Example:
conditions:
author_member:
source: group
condition: not_member_of
source_id: 9970
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 | yes |
Example:
conditions:
assignee_member:
source: group
condition: not_member_of
source_id: 9970
Accepts the name of a source branch to filter upon.
Example:
conditions:
source_branch: 'feature-branch'
Accepts the name of a target branch to filter upon.
Example:
conditions:
target_branch: 'master'
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:
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:
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 for the list of currently available API.
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:
limits:
most_recent: 50
Used to declare an action to be carried out on a resource if all conditions are satisfied.
Available action types:
labels
actionremove_labels
actionstatus
actionmention
actionmove
actioncomment
actioncomment_type
action optionsummarize
actionAdds a number of labels to the resource.
Accepts an array of strings. Each element is the name of a label to add.
Example:
actions:
labels:
- feature proposal
- awaiting feedback
Removes a number of labels from the resource.
Accepts an array of strings. Each element is the name of a label to remove.
Example:
actions:
remove_labels:
- feature proposal
- awaiting feedback
Changes the status of the resource.
Accepts a string.
State transition | Type | Value |
---|---|---|
Close the resource | string | close |
Reopen the resource | string | reopen |
Example:
actions:
status: close
Mentions a number of users.
Accepts an array of strings. Each element is the username of a user to mention.
Example:
actions:
mention:
- rymai
- markglenfletcher
Moves an issue (merge request is not supported yet) to the specified project.
Accepts a string containing the target project path.
Example:
actions:
move: target/project_path
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 dateupdated_at
: the resource's last update dateclosed_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 milestonelabels
: the resource's labels as ~label1, ~label2
upvotes
: the resources's upvotes countdownvotes
: the resources's downvotes counttitle
: the resource's titleweb_url
: the web URL pointing to the resourcetype
: the type of the resources. For now, only issues
and
merge_requests
are supported.If the resource doesn't respond to the placeholder, or if the field is nil
,
the placeholder is not replaced.
Example without placeholders:
actions:
comment: |
Closing this issue automatically
Example with placeholders:
actions:
comment: |
{{author}} Are you still interested in finishing this merge request?
Determines the type of comment to be added to the resource.
The following comment types are supported:
comment
(default): creates a regular comment on the resourcethread
: starts a resolvable thread (discussion) on the resourceFor merge requests, if comment_type
is set to thread
, we can also configure that all threads should be resolved before merging, therefore this comment can prevent it from merging.
Example:
actions:
comment_type: thread
comment: |
{{author}} Are you still interested in finishing this merge request?
GitLab's quick actions feature is available in Core. All of the operations supported by executing a quick action can be carried out via the comment action.
If GitLab triage does not support an operation natively, it may be possible via a quick action in a comment.
For example:
resource_rules:
issues:
rules:
- name: Mark bugs as confidential
conditions:
state: opened
ruby: !resource[:confidential]
labels:
- bug
actions:
comment: |
/confidential
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:
actions:
comment: |
Please move this to %"#{milestone.succ.title}".
See 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: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.
Generates an issue summarizing what was triaged.
Accepts a hash of fields.
Field | Type | Description | Required | Placeholders | Ruby expression | Default |
---|---|---|---|---|---|---|
title |
string | The title of the generated issue | yes | yes | no | |
destination |
integer or string | The project ID or path to create the generated issue in | no | no | no | source project |
item |
string | Template representing each triaged resource | no | yes | yes | |
summary |
string | The description of the generated issue | no | Only {{title}} , {{items}} , {{type}}
|
yes | |
redact_confidential_resources |
boolean | Whether redact fields for confidential resources | no | no | no | true |
The following placeholders are supported for summary
:
title
: The title of the generated issueitems
: Concatenated markdown separated by a newline for each item
type
: The resource type for the summary. For now issues
or
merge_requests
Note:
- Both
item
andsummary
fields act like a comment action, therefore Ruby expression is supported.- Placeholders work regularly for
item
, but forsummary
only{{title}}
,{{items}}
,{{type}}
are supported because it's not tied to a particular resource like the comment action.- No issues will be created if:
- the specific policy doesn't yield any resources; or
- the source type is a group and
destination
is not set.redact_confidential_resources
defaults totrue
, so fields on confidential resources will be converted to(confidential)
except for{{web_url}}
. Setting it tofalse
will reveal the confidential fields. This will be useful if the summary is confidential itself (not implemented yet), or if we're posting to another private project (not implemented yet).
Example:
resource_rules:
issues:
rules:
- name: Issues require labels
limits:
most_recent: 15
actions:
summarize:
title: |
#{resource[:type].capitalize} require labels
item: |
- [ ] [{{title}}]({{web_url}}) {{labels}}
summary: |
The following {{type}} 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:
Issues require labels
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"
Summary policies are special policies that join multiple rule policies together
to create a summary issue with all the sub-policies' summaries.
They have the same structure as Rule policies that define actions.summarize
.
One key difference is that the {{items}}
placeholder represents the array of
sub-policies' summary.
Note that only the summarize
keys in the sub-policies' actions
is used. Any
other keys (e.g. mention
, comment
, labels
etc.) are ignored.
You can define such policy as follows:
resource_rules:
issues:
summaries:
- name: Newest and oldest issues summary
actions:
summarize:
title: "Newest and oldest {{type}} summary"
summary: |
Please triage the following {{type}}:
{{items}}
Please take care of them before the end of #{7.days.from_now.strftime('%Y-%m-%d')}
/label ~"needs attention"
rules:
- name: New issues
conditions:
state: opened
limits:
most_recent: 2
actions:
summarize:
item: "- [ ] [{{title}}]({{web_url}}) {{labels}}"
summary: |
Please triage the following new {{type}}:
{{items}}
- name: Old issues
conditions:
state: opened
limits:
oldest: 2
actions:
summarize:
item: "- [ ] [{{title}}]({{web_url}}) {{labels}}"
summary: |
Please triage the following old {{type}}:
{{items}}
Which could generate an issue like:
Newest and oldest issues summary
Please triage the following issues:
Please triage the following new issues:
- [ ] [A new issue](http://example.com/group/project/issues/4)
- [ ] [Another new issue](http://example.com/group/project/issues/3) ~"label B", ~"label C"
Please triage the following old issues:
- [ ] [An old issue](http://example.com/group/project/issues/1) ~"label A", ~"label B"
- [ ] [Another old issue](http://example.com/group/project/issues/2) ~"label C"
Please take care of them before the end of 2000-01-01
/label ~"needs attention"
Note: If a specific policy doesn't yield any resources, it will not generate the corresponding description. If all policies yield no resources, then no issues will be created.
Here's a list of currently available Ruby expression API:
Issue
and MergeRequest
(the context)Name | Return type | Description |
---|---|---|
resource | Hash | The hash containing the raw data of the resource |
author | String | The username of the resource author |
milestone | Milestone | The milestone attached to the resource |
labels | [Label] | A list of labels, having only names |
labels_with_details | [Label] | A list of labels which has more information loaded from another API request |
labels_chronologically | [Label] | Same as labels_with_details but sorted chronologically |
label_events | [LabelEvent] | A list of label events on the resource |
instance_version | InstanceVersion | The version for the GitLab instance we're triaging with |
project_path | String | The path with namespace to the issues or merge requests project |
full_resource_reference | String | A full reference incuding project path to the issue or merge request |
MergeRequest
(merge request context)Method | Return type | Description |
---|---|---|
first_contribution? | Boolean |
true if it's the author's first contribution to the project; false otherwise. This API requires an additional API request for the merge request, thus would be slower. |
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 |
closed? | Boolean |
true if state is closed ; false otherwise |
started? | Boolean |
true if start_date exists and in the past; false otherwise |
expired? | Boolean |
true if due_date exists and in the past; false otherwise |
in_progress? | Boolean |
true if started? and !expired ; false otherwise |
Label
Method | Return type | Description |
---|---|---|
id | Integer | The id of the label |
project_id | Integer | The project id of the label if available |
group_id | Integer | The group id of the label if available |
name | String | The name of the label |
description | String | The description of the label |
color | String | The color of the label in RGB |
priority | Integer | The priority of the label |
added_at | Time | When the label was added to the resource |
LabelEvent
Method | Return type | Description |
---|---|---|
id | Integer | The id of the label event |
resource_type | String | The resource type of the event. Could be Issue or MergeRequest
|
resource_id | Integer | The id of the resource |
action | String | The action of the event. Could be add or remove
|
created_at | Time | When the event happened |
InstanceVersion
Method | Return type | Description |
---|---|---|
version | String | The full string of version. e.g. 11.3.0-rc11-ee
|
version_short | String | The short string of version. e.g. 11.3
|
revision | String | The revision of GitLab. e.g. 231b0c7
|
gem install gitlab-triage
gitlab-triage --help
Will show:
Usage: gitlab-triage [options]
-n, --dry-run Don't actually update anything, just print
-f, --policies-file [string] A valid policies YML file
-s, --source [type] The source type between [ projects or groups ], default value: projects
-i, --source-id [string] Source ID or path
-p, --project-id [string] [Deprecated] A project ID or path, please use `--source-id`
-t, --token [string] A valid API token
-H, --host-url [string] A valid host url
-r, --require [string] Require a file before performing
-d, --debug Print debug information
-h, --help Print help message
--all-projects Process all projects visible to `--token`
--init Initialize the project with a policy file
--init-ci Initialize the project with a .gitlab-ci.yml file
Triaging against a specific project:
gitlab-triage --dry-run --token $API_TOKEN --source-id gitlab-org/triage
Triaging against a whole group:
gitlab-triage --dry-run --token $API_TOKEN --source-id gitlab-org --source groups
Triaging against an entire instance:
gitlab-triage --dry-run --token $API_TOKEN --all-projects
Note: The
--all-projects
option will process all resources for all projects visible to the specified$API_TOKEN
You can enforce policies using a scheduled pipeline:
run:triage:triage:
stage: triage
script:
- gem install gitlab-triage
- gitlab-triage --token $API_TOKEN --source-id $CI_PROJECT_PATH
only:
- schedules
Note: You can use the
--init-ci
option to add an example.gitlab-ci.yml
file to your project
Yes, you can override the host url using the following options:
gitlab-triage --dry-run --token $API_TOKEN --source-id gitlab-org/triage --host-url https://gitlab.host.com
host_url: https://gitlab.host.com
resource_rules:
You can take the advantage of command line option -r
or --require
to
load a Ruby file before performing the actions. This allows you to do
whatever you want. For example, you can put this in a file like my_plugin.rb
:
module MyPlugin
def has_severity_label?
labels.grep(/^S\d+$/).any?
end
def has_priority_label?
labels.grep(/^P\d+$/).any?
end
def labels
resource[:labels]
end
end
Gitlab::Triage::Resource::Context.include MyPlugin
And then run it with:
gitlab-triage -r ./my_plugin.rb --token $API_TOKEN --source-id gitlab-org/triage
This allows you to use has_severity_label?
in the Ruby condition:
resource_rules:
issues:
rules:
- name: Apply default severity or priority labels
conditions:
ruby: |
!has_severity_label? || !has_priority_label?
actions:
comment: |
#{'/label ~S3' unless has_severity_label?}
#{'/label ~P3' unless has_priority_label?}
Please refer to the Contributing Guide.
Please refer to the Release Process.