gitlab-triage 1.56.1 → 1.56.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/lib/gitlab/triage/engine.rb +81 -111
- data/lib/gitlab/triage/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 51a92e16690b0c2f5453cc6a63f69afc72664c457e66be02a4c0ea976ceedddd
|
|
4
|
+
data.tar.gz: 36f7c9d84716e73d5f329a98ab06a8e09093b05e77e77dcb9ef8aa0a7cf62a93
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e3c94b3fc92a0e66df471d4912f6c97527f1bce6de7b872ee01818fa7eae0bb54defb9ec2c64dcb295997a6d755bd6f9932b4fe2436349c6a6c275ab9e6b9462
|
|
7
|
+
data.tar.gz: 119e10a479dbfbcd9f0e51ffc17e65582ffa058129c0077175f843a2e450dcfb03e49c8b8a963f15b3e08c3ecfc40fe77c3dc22c23fb5ef540d3c04925610957
|
data/Gemfile.lock
CHANGED
data/lib/gitlab/triage/engine.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'active_support/all'
|
|
4
4
|
require 'active_support/inflector'
|
|
5
|
+
require 'globalid'
|
|
5
6
|
|
|
6
7
|
require_relative 'expand_condition'
|
|
7
8
|
require_relative 'filters/issue_date_conditions_filter'
|
|
@@ -409,14 +410,14 @@ module Gitlab
|
|
|
409
410
|
end
|
|
410
411
|
|
|
411
412
|
ExpandCondition.perform(rule_conditions(rule_definition)) do |expanded_conditions|
|
|
412
|
-
|
|
413
|
+
status_by_id = pre_filter_by_work_item_status!(resource_type, expanded_conditions)
|
|
413
414
|
|
|
414
415
|
# retrieving the resources for every rule is inefficient
|
|
415
416
|
# however, previous rules may affect those upcoming
|
|
416
417
|
resources = options.resources ||
|
|
417
418
|
fetch_resources(resource_type, expanded_conditions, rule_definition)
|
|
418
419
|
|
|
419
|
-
apply_work_item_status!(resource_type, expanded_conditions, rule_definition, resources,
|
|
420
|
+
apply_work_item_status!(resource_type, expanded_conditions, rule_definition, resources, status_by_id)
|
|
420
421
|
|
|
421
422
|
# In some filters/actions we want to know which resource type it is
|
|
422
423
|
attach_resource_type(resources, resource_type)
|
|
@@ -452,6 +453,7 @@ module Gitlab
|
|
|
452
453
|
|
|
453
454
|
if rule_definition[:api] == 'graphql'
|
|
454
455
|
graphql_query_options = { source: source_full_path }
|
|
456
|
+
pre_filter_status_ids = expanded_conditions.delete(:work_item_status_ids)
|
|
455
457
|
|
|
456
458
|
if options.resource_reference
|
|
457
459
|
expanded_conditions[:iids] = options.resource_reference[1..]
|
|
@@ -463,25 +465,16 @@ module Gitlab
|
|
|
463
465
|
graphql_query = build_graphql_query(resource_type, expanded_conditions, rule_definition, true)
|
|
464
466
|
|
|
465
467
|
resources = graphql_network.query(graphql_query, **graphql_query_options)
|
|
468
|
+
resources = narrow_to_pre_filtered(resources, nil, pre_filter_status_ids)
|
|
466
469
|
else
|
|
467
470
|
# FIXME: Epics listing endpoint doesn't support filtering by `iids`, so instead we
|
|
468
471
|
# get a single epic when `--resource-reference` is given for epics.
|
|
469
472
|
# Because of that, the query could return a single epic, so we make sure we get an array.
|
|
470
473
|
pre_filter_iids = expanded_conditions.delete(:iids)
|
|
474
|
+
pre_filter_status_ids = expanded_conditions.delete(:work_item_status_ids)
|
|
471
475
|
|
|
472
476
|
resources = Array(network.query_api(build_get_url(resource_type, expanded_conditions)))
|
|
473
|
-
|
|
474
|
-
# Narrow the REST results to the pre-filtered IID set, when a pre-filter
|
|
475
|
-
# ran. The contract is carried by the value itself: `nil` means no
|
|
476
|
-
# pre-filter ran (keep all resources), while any array - including `[]` -
|
|
477
|
-
# means "narrow to exactly these IIDs". An empty array therefore yields
|
|
478
|
-
# zero resources, which is correct when a pre-filter (e.g. work_item_status)
|
|
479
|
-
# ran and matched nothing. `present?` cannot express this because it is
|
|
480
|
-
# false for `[]`.
|
|
481
|
-
unless pre_filter_iids.nil?
|
|
482
|
-
filtered_iids = pre_filter_iids.map(&:to_s)
|
|
483
|
-
resources = resources.select { |r| filtered_iids.include?(r['iid'].to_s) }
|
|
484
|
-
end
|
|
477
|
+
resources = narrow_to_pre_filtered(resources, pre_filter_iids, pre_filter_status_ids)
|
|
485
478
|
|
|
486
479
|
iids = resources.pluck('iid').map(&:to_s)
|
|
487
480
|
expanded_conditions[:iids] = iids
|
|
@@ -495,6 +488,29 @@ module Gitlab
|
|
|
495
488
|
resources
|
|
496
489
|
end
|
|
497
490
|
|
|
491
|
+
# Narrow the fetched results to a pre-filtered set, when a pre-filter ran.
|
|
492
|
+
# The contract is carried by each value: `nil` means that pre-filter did not
|
|
493
|
+
# run (keep all resources), while any array - including `[]` - means "narrow
|
|
494
|
+
# to exactly these". An empty array therefore yields zero resources, which is
|
|
495
|
+
# correct when a pre-filter (e.g. work_item_status) ran and matched nothing;
|
|
496
|
+
# `present?` cannot express this because it is false for `[]`.
|
|
497
|
+
#
|
|
498
|
+
# The work_item_status pre-filter narrows by the globally-unique id, so it
|
|
499
|
+
# cannot conflate same-iid issues from different projects of a group.
|
|
500
|
+
def narrow_to_pre_filtered(resources, pre_filter_iids, pre_filter_status_ids)
|
|
501
|
+
unless pre_filter_status_ids.nil?
|
|
502
|
+
filtered_ids = pre_filter_status_ids.map(&:to_s)
|
|
503
|
+
resources = resources.select { |r| filtered_ids.include?(r[:id].to_s) }
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
unless pre_filter_iids.nil?
|
|
507
|
+
filtered_iids = pre_filter_iids.map(&:to_s)
|
|
508
|
+
resources = resources.select { |r| filtered_iids.include?(r[:iid].to_s) }
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
resources
|
|
512
|
+
end
|
|
513
|
+
|
|
498
514
|
def fetch_work_items(expanded_conditions)
|
|
499
515
|
conditions = expanded_conditions.dup
|
|
500
516
|
conditions[:iids] = options.resource_reference[1..] if options.resource_reference
|
|
@@ -729,32 +745,33 @@ module Gitlab
|
|
|
729
745
|
end
|
|
730
746
|
|
|
731
747
|
# When filtering by work_item_status, query workItems with server-side
|
|
732
|
-
# status filtering first to narrow the
|
|
748
|
+
# status filtering first to narrow the resource set before the main fetch.
|
|
733
749
|
# This avoids fetching all resources just to discard most of them.
|
|
750
|
+
#
|
|
751
|
+
# We narrow by the globally-unique work item id, not the iid: a group
|
|
752
|
+
# source flattens work items from every descendant project into one list,
|
|
753
|
+
# and iids are unique only within a project, so two projects can share an
|
|
754
|
+
# iid. The id side-steps that collision.
|
|
734
755
|
def pre_filter_by_work_item_status!(resource_type, expanded_conditions)
|
|
735
|
-
return
|
|
736
|
-
|
|
737
|
-
pre_filter_iids = fetch_work_item_iids_by_status(expanded_conditions[:work_item_status])
|
|
756
|
+
return unless resource_type == 'issues' && expanded_conditions.key?(:work_item_status)
|
|
738
757
|
|
|
739
|
-
expanded_conditions[:
|
|
740
|
-
|
|
741
|
-
Array(expanded_conditions[:iids]).map(&:to_s) & pre_filter_iids
|
|
742
|
-
else
|
|
743
|
-
pre_filter_iids
|
|
744
|
-
end
|
|
758
|
+
status_by_id = fetch_work_item_status_by_id(expanded_conditions[:work_item_status])
|
|
759
|
+
expanded_conditions[:work_item_status_ids] = status_by_id.keys
|
|
745
760
|
|
|
746
|
-
|
|
761
|
+
status_by_id
|
|
747
762
|
end
|
|
748
763
|
|
|
749
|
-
|
|
764
|
+
# The pre-filter (pre_filter_by_work_item_status!) always runs before this
|
|
765
|
+
# for an issues rule that filters by work_item_status, and returns a map of
|
|
766
|
+
# id to matched status name. We stamp each resource from that map instead of
|
|
767
|
+
# a second, iid-keyed status query: the pre-filter already knows the answer,
|
|
768
|
+
# and keying by the globally-unique id avoids the group iid collision that a
|
|
769
|
+
# flattened `iids:` query would reintroduce.
|
|
770
|
+
def apply_work_item_status!(resource_type, expanded_conditions, rule_definition, resources, status_by_id)
|
|
750
771
|
return unless resource_type == 'issues' && needs_work_item_status?(expanded_conditions, rule_definition)
|
|
751
772
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
if status_pre_filtered && status_values.size == 1
|
|
755
|
-
resources.each { |r| r[:work_item_status] = status_values.first }
|
|
756
|
-
else
|
|
757
|
-
decorate_resources_with_work_item_status(resources)
|
|
773
|
+
resources.each do |resource|
|
|
774
|
+
resource[:work_item_status] = status_by_id[resource[:id].to_s]
|
|
758
775
|
end
|
|
759
776
|
end
|
|
760
777
|
|
|
@@ -762,24 +779,40 @@ module Gitlab
|
|
|
762
779
|
conditions.key?(:work_item_status)
|
|
763
780
|
end
|
|
764
781
|
|
|
782
|
+
# A group's `workItems` connection returns only the group's own work items
|
|
783
|
+
# (epics), whose IIDs do not match the project-scoped issue IIDs the issues
|
|
784
|
+
# fetch returns. `includeDescendants: true` pulls in the descendant project
|
|
785
|
+
# issues so the status IIDs line up. The argument does not exist on
|
|
786
|
+
# `project.workItems`, so it is only emitted for a group source.
|
|
787
|
+
def work_item_descendants_argument
|
|
788
|
+
options.source == 'groups' ? 'includeDescendants: true, ' : ''
|
|
789
|
+
end
|
|
790
|
+
|
|
765
791
|
# Pre-filter: query workItems with server-side status filtering to get only
|
|
766
|
-
# the
|
|
767
|
-
# items won't pass the status filter.
|
|
768
|
-
|
|
792
|
+
# the items that match, avoiding fetching the full resource set when most
|
|
793
|
+
# items won't pass the status filter. Because it queries one status at a
|
|
794
|
+
# time, it also learns which status each id matched, and returns that as a
|
|
795
|
+
# map of globally-unique id (from the work item GID) to status name. The id
|
|
796
|
+
# keys stay correct across a group's descendant projects where iids can
|
|
797
|
+
# collide, and the map lets the caller stamp statuses without a second
|
|
798
|
+
# iid-keyed query. When several statuses match the same id, the last one
|
|
799
|
+
# queried wins; a work item has a single status, so this does not happen in
|
|
800
|
+
# practice.
|
|
801
|
+
def fetch_work_item_status_by_id(status_condition)
|
|
769
802
|
status_names = Array(status_condition)
|
|
770
|
-
|
|
803
|
+
status_by_id = {}
|
|
771
804
|
|
|
772
805
|
status_names.each do |status_name|
|
|
773
806
|
query = <<~GRAPHQL
|
|
774
807
|
query($source: ID!, $after: String, $statusName: String!) {
|
|
775
808
|
#{options.source.singularize}(fullPath: $source) {
|
|
776
|
-
workItems(status: { name: $statusName }, after: $after, first: 100) {
|
|
809
|
+
workItems(status: { name: $statusName }, #{work_item_descendants_argument}after: $after, first: 100) {
|
|
777
810
|
pageInfo {
|
|
778
811
|
hasNextPage
|
|
779
812
|
endCursor
|
|
780
813
|
}
|
|
781
814
|
nodes {
|
|
782
|
-
|
|
815
|
+
id
|
|
783
816
|
}
|
|
784
817
|
}
|
|
785
818
|
}
|
|
@@ -797,8 +830,9 @@ module Gitlab
|
|
|
797
830
|
|
|
798
831
|
work_items = Array.wrap(response[:results])
|
|
799
832
|
work_items.each do |wi|
|
|
800
|
-
|
|
801
|
-
|
|
833
|
+
gid = wi.is_a?(Hash) ? (wi['id'] || wi[:id]) : wi.try(:id)
|
|
834
|
+
id = numeric_id_from_gid(gid)
|
|
835
|
+
status_by_id[id.to_s] = status_name if id
|
|
802
836
|
end
|
|
803
837
|
|
|
804
838
|
break unless response[:more_pages]
|
|
@@ -807,80 +841,16 @@ module Gitlab
|
|
|
807
841
|
end
|
|
808
842
|
end
|
|
809
843
|
|
|
810
|
-
|
|
811
|
-
end
|
|
812
|
-
|
|
813
|
-
# Status is a work item capability not available on the IssueType GraphQL type
|
|
814
|
-
# or the REST API. We fetch it via a separate workItems query and merge it back
|
|
815
|
-
# by IID, similar to how decorate_resources_with_graphql_data works for other fields.
|
|
816
|
-
# We use widgets (not features) for backward compatibility with GitLab versions
|
|
817
|
-
# before the features field was introduced in 18.9.
|
|
818
|
-
def decorate_resources_with_work_item_status(resources)
|
|
819
|
-
return if resources.empty?
|
|
820
|
-
|
|
821
|
-
iids = resources.filter_map { |r| (r['iid'] || r[:iid])&.to_s }
|
|
822
|
-
return if iids.empty?
|
|
823
|
-
|
|
824
|
-
status_by_iid = fetch_work_item_statuses_by_iid(iids)
|
|
825
|
-
|
|
826
|
-
resources.each do |resource|
|
|
827
|
-
iid = (resource['iid'] || resource[:iid]).to_s
|
|
828
|
-
resource[:work_item_status] = status_by_iid[iid]
|
|
829
|
-
end
|
|
844
|
+
status_by_id
|
|
830
845
|
end
|
|
831
846
|
|
|
832
|
-
#
|
|
833
|
-
#
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
#{options.source.singularize}(fullPath: $source) {
|
|
838
|
-
workItems(iids: $iids, after: $after, first: 100) {
|
|
839
|
-
pageInfo {
|
|
840
|
-
hasNextPage
|
|
841
|
-
endCursor
|
|
842
|
-
}
|
|
843
|
-
nodes {
|
|
844
|
-
iid
|
|
845
|
-
widgets(onlyTypes: [STATUS]) {
|
|
846
|
-
... on WorkItemWidgetStatus {
|
|
847
|
-
status {
|
|
848
|
-
id
|
|
849
|
-
name
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
GRAPHQL
|
|
858
|
-
|
|
859
|
-
status_by_iid = {}
|
|
860
|
-
|
|
861
|
-
iids.each_slice(100) do |iids_batch|
|
|
862
|
-
after_cursor = nil
|
|
863
|
-
|
|
864
|
-
loop do
|
|
865
|
-
response = graphql_network.adapter.query_raw(
|
|
866
|
-
query,
|
|
867
|
-
resource_path: [options.source.singularize, 'workItems'],
|
|
868
|
-
variables: { source: source_full_path, iids: iids_batch, after: after_cursor }
|
|
869
|
-
)
|
|
870
|
-
|
|
871
|
-
Array.wrap(response[:results]).each do |wi|
|
|
872
|
-
wi = wi.deep_transform_keys(&:underscore).with_indifferent_access
|
|
873
|
-
status_widget = Array.wrap(wi[:widgets]).find { |w| w[:status].present? }
|
|
874
|
-
status_by_iid[wi[:iid].to_s] = status_widget.dig(:status, :name) if status_widget
|
|
875
|
-
end
|
|
876
|
-
|
|
877
|
-
break unless response[:more_pages]
|
|
878
|
-
|
|
879
|
-
after_cursor = response[:end_cursor]
|
|
880
|
-
end
|
|
881
|
-
end
|
|
847
|
+
# A work item GID (`gid://gitlab/WorkItem/1234`) carries the same model id
|
|
848
|
+
# the REST issues endpoint returns as `id`. Parsed with GlobalID, matching
|
|
849
|
+
# GraphqlNetwork#extract_id_from_global_id.
|
|
850
|
+
def numeric_id_from_gid(gid)
|
|
851
|
+
return if gid.blank?
|
|
882
852
|
|
|
883
|
-
|
|
853
|
+
GlobalID.parse(gid).model_id
|
|
884
854
|
end
|
|
885
855
|
|
|
886
856
|
def build_graphql_query(resource_type, conditions, rule_definition, graphql_only = false)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gitlab-triage
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.56.
|
|
4
|
+
version: 1.56.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GitLab
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|