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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9420b956227ae89432c05b78279b5c8267bf98e721c4feb2cde5fee21a7d611a
4
- data.tar.gz: ffde105e64820dca1e873e4c368bbe65d4039080aba864e114e67e7c6d3efaa3
3
+ metadata.gz: 51a92e16690b0c2f5453cc6a63f69afc72664c457e66be02a4c0ea976ceedddd
4
+ data.tar.gz: 36f7c9d84716e73d5f329a98ab06a8e09093b05e77e77dcb9ef8aa0a7cf62a93
5
5
  SHA512:
6
- metadata.gz: 4475319216eee8897599eef75260c58e3d677043d3a8b86cbe1ef44b6146faafb967786d3004731879b8a9c4996a21620a50956196f978aa94c704f9e2c2ef96
7
- data.tar.gz: 42b71bf2233e9daeeec8834b2bb3d651f620f758f83cb3e4e17701208aa40fbf55473532d5eff574dde327afc4c3d97029c370ee5c34198d22bf784a51af932d
6
+ metadata.gz: e3c94b3fc92a0e66df471d4912f6c97527f1bce6de7b872ee01818fa7eae0bb54defb9ec2c64dcb295997a6d755bd6f9932b4fe2436349c6a6c275ab9e6b9462
7
+ data.tar.gz: 119e10a479dbfbcd9f0e51ffc17e65582ffa058129c0077175f843a2e450dcfb03e49c8b8a963f15b3e08c3ecfc40fe77c3dc22c23fb5ef540d3c04925610957
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab-triage (1.56.1)
4
+ gitlab-triage (1.56.2)
5
5
  activesupport (>= 5.1)
6
6
  globalid (~> 1.0, >= 1.0.1)
7
7
  graphql (< 2.1.0)
@@ -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
- status_pre_filtered = pre_filter_by_work_item_status!(resource_type, expanded_conditions)
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, status_pre_filtered)
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 IID set before the main fetch.
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 false unless resource_type == 'issues' && expanded_conditions.key?(:work_item_status)
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[:iids] =
740
- if expanded_conditions[:iids].present?
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
- true
761
+ status_by_id
747
762
  end
748
763
 
749
- def apply_work_item_status!(resource_type, expanded_conditions, rule_definition, resources, status_pre_filtered)
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
- status_values = Array(expanded_conditions[:work_item_status])
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 IIDs that match, avoiding fetching the full resource set when most
767
- # items won't pass the status filter.
768
- def fetch_work_item_iids_by_status(status_condition)
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
- all_iids = []
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
- iid
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
- iid = wi.is_a?(Hash) ? (wi['iid'] || wi[:iid]) : wi.try(:iid)
801
- all_iids << iid.to_s if iid
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
- all_iids.uniq
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
- # The workItems connection caps `first` at 100, so we request the status
833
- # for the given IIDs in batches and paginate within each batch.
834
- def fetch_work_item_statuses_by_iid(iids)
835
- query = <<~GRAPHQL
836
- query($source: ID!, $iids: [String!], $after: String) {
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
- status_by_iid
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)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Gitlab
4
4
  module Triage
5
- VERSION = '1.56.1'
5
+ VERSION = '1.56.2'
6
6
  end
7
7
  end
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.1
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-02 00:00:00.000000000 Z
11
+ date: 2026-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport