plan_my_stuff 0.23.0 → 0.24.0

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: e6c0f0590ab0ae66923b771acd03e238d78ed2b535a24efc1ad8047774874914
4
- data.tar.gz: 991920b03d4f7b2417ed51c213c0b3a9368772dbc552d5d1f17d80a512207b94
3
+ metadata.gz: 1cf33c92cfa763f5ace87a1419cf8c7369f27e8adc10d1c092c044b99d151a4a
4
+ data.tar.gz: 3b2a482534b9f9094d550385c064f50f4dc37db3c2a6361e4aa899d4b4dc5713
5
5
  SHA512:
6
- metadata.gz: 712c0847e85926474c0331211de128e1732e99672f79cf4cc56f6ec3313ab814b0bef15cfa3c51d360d78abac5f99825c20ec29be9aa274265a37ea271648293
7
- data.tar.gz: 153ecbe20c5bcc2c9d5b25cf85ded6fbe27ca6ea6b881e9b1e62bf04e3c538e5baf50e604f97092ec9f741bfc9cc0cbb29ea72bc86f42fb879ac0d91fb7e5180
6
+ metadata.gz: ef36dbcfb77d4c3763d2165993c50fb9f4f7e8b0729a1efc1d63fb9155abb039bf9db92fb59a49d3302ebfec024bafc19dd15b54be84a2d7919040405a10803f
7
+ data.tar.gz: beae8f4926a95e4a47786c4ffefc886300f255466d7e08a7e607f444e91883e920b3bf60d666d33b539eed2dac7a1241f64cd23ae42ebe292a8a113ce150de8d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.24.0
4
+
5
+ ### Added
6
+
7
+ - `PlanMyStuff::Issue.list` and `PlanMyStuff::Issue.count` now accept `issue_type:` (String / Symbol) and
8
+ `issue_fields:` (Hash keyed by display name) filter kwargs. `issue_type:` resolves symbol nicknames through
9
+ `ISSUE_TYPE_NICKNAMES` and `config.issue_types`; Arrays raise `ArgumentError` since GitHub's REST `type` param
10
+ and Search `type:` qualifier each accept only one value. `issue_fields:` accepts scalar (equality) or `Range`
11
+ (date / numeric bounds -- inclusive
12
+ `..` -> `>=`/`<=`, exclusive `...` -> `>=`/`<`, beginless / endless ranges drop the unbounded side) values and ANDs
13
+ multiple constraints together. Composes with the existing `priority_list:` filter into the same
14
+ `issue_field_values` (REST) / `field.<slug>:` (Search) qualifier list. Search-API calls flip
15
+ `advanced_search=true` when any field qualifier is present. `issue_fields:` raises
16
+ `IssueFieldsNotEnabledError` when `config.issue_fields_enabled` is `false` (closes #54).
17
+
18
+ ## 0.23.1
19
+
20
+ ### Added
21
+
22
+ - `PlanMyStuff::UserResolver.from_github_login(login)` - inverse of `config.github_login_for`. Returns the
23
+ resolved user object whose configured GitHub login matches `login`, or `nil` when `login` is blank or
24
+ unmapped.
25
+
26
+ ### Changed
27
+
28
+ - `Webhooks::GithubController` now forwards the actor through to `Pipeline.take!`'s `user:` kwarg from
29
+ `handle_issue_assigned` (using `assignee.login`) and `handle_draft_opened` (using the PR author). The
30
+ resolved user lands on the `pipeline_started.plan_my_stuff` notification payload.
31
+ - `Issues::TakesController#create` now forwards `pms_current_user` to `Pipeline.take!`'s `user:` kwarg.
32
+
3
33
  ## 0.23.0
4
34
 
5
35
  ### Added
@@ -17,7 +17,7 @@ module PlanMyStuff
17
17
  project_item = PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue.number)
18
18
  project_item ||= add_to_pipeline(issue)
19
19
 
20
- PlanMyStuff::Pipeline.take!(project_item)
20
+ PlanMyStuff::Pipeline.take!(project_item, user: pms_current_user)
21
21
  assign_current_user(project_item)
22
22
 
23
23
  yield(project_item) if block_given?
@@ -119,7 +119,7 @@ module PlanMyStuff
119
119
 
120
120
  number = PlanMyStuff::Pipeline.resolve_pipeline_project_number!
121
121
  project_item = PlanMyStuff::ProjectItem.create!(issue, project_number: number)
122
- PlanMyStuff::Pipeline.take!(project_item)
122
+ PlanMyStuff::Pipeline.take!(project_item, user: PlanMyStuff::UserResolver.from_github_login(assignee_login))
123
123
  end
124
124
 
125
125
  # Removes the issue from the pipeline project when the LAST assignee is removed. If any assignees remain,
@@ -253,7 +253,7 @@ module PlanMyStuff
253
253
  issue = PlanMyStuff::Issue.find(issue_number, repo: repo)
254
254
 
255
255
  if PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue_number).nil?
256
- ensure_in_pipeline_at_started(issue)
256
+ ensure_in_pipeline_at_started(issue, pr_author)
257
257
  end
258
258
 
259
259
  assign_pr_author(issue, pr_author) if pr_author.present? && issue.assignees.empty?
@@ -271,10 +271,11 @@ module PlanMyStuff
271
271
  # (Pipeline.take!'s guard would otherwise leave an orphan project item behind).
272
272
  #
273
273
  # @param issue [PlanMyStuff::Issue]
274
+ # @param actor_login [String, nil] GitHub login of the webhook actor, mapped to a user via +github_login_for+
274
275
  #
275
276
  # @return [void]
276
277
  #
277
- def ensure_in_pipeline_at_started(issue)
278
+ def ensure_in_pipeline_at_started(issue, actor_login = nil)
278
279
  if issue.approvals_required? && !issue.fully_approved?
279
280
  Rails.logger.info("[PlanMyStuff] Issue ##{issue.number} has pending approvals, skipping pipeline add")
280
281
  return
@@ -282,7 +283,7 @@ module PlanMyStuff
282
283
 
283
284
  number = PlanMyStuff::Pipeline.resolve_pipeline_project_number!
284
285
  project_item = PlanMyStuff::ProjectItem.create!(issue, project_number: number)
285
- PlanMyStuff::Pipeline.take!(project_item)
286
+ PlanMyStuff::Pipeline.take!(project_item, user: PlanMyStuff::UserResolver.from_github_login(actor_login))
286
287
  end
287
288
 
288
289
  # @param issue [PlanMyStuff::Issue]
@@ -296,11 +296,29 @@ module PlanMyStuff
296
296
 
297
297
  # Lists GitHub issues with optional filters and pagination.
298
298
  #
299
- # @raise [ArgumentError] when +priority_list: false+ is passed
299
+ # +issue_fields:+ is a Hash keyed by GitHub Issue Field display name (String / Symbol). Each value is either
300
+ # a scalar (equality match -- Date / Time are emitted as ISO 8601, everything else as +to_s+) or a +Range+ for
301
+ # numeric / date bounds:
302
+ #
303
+ # - +Date.parse('2026-01-01')..Date.today+ -> +start-date:>=2026-01-01,start-date:<=2026-05-21+
304
+ # - +Date.parse('2026-01-01')...Date.today+ -> +start-date:>=2026-01-01,start-date:<2026-05-21+ (exclusive end)
305
+ # - +..Date.today+ (beginless) / +Date.parse('2026-01-01')..+ (endless) drop the unbounded side
306
+ #
307
+ # Multiple field constraints AND together. Composes with the existing +priority_list:+ filter: both feed the
308
+ # same +issue_field_values+ query param.
309
+ #
310
+ # @raise [ArgumentError] when +priority_list: false+ is passed, or when +issue_type:+ is an Array (GitHub's
311
+ # REST +type+ param only accepts a single value)
312
+ # @raise [PlanMyStuff::IssueFieldsNotEnabledError] when +issue_fields:+ is passed and
313
+ # +config.issue_fields_enabled+ is +false+
300
314
  #
301
315
  # @param repo [Symbol, String, nil] defaults to config.default_repo
302
316
  # @param state [Symbol] :open, :closed, or :all
303
317
  # @param labels [Array<String>]
318
+ # @param issue_type [String, Symbol, nil] a single GitHub issue type name. Symbols resolve through
319
+ # +ISSUE_TYPE_NICKNAMES+ then +config.issue_types+ for org-specific renames.
320
+ # @param issue_fields [Hash{String,Symbol => Object,Range,nil}, nil] GitHub Issue Field equality / range
321
+ # filters. See description for the value shapes the gem accepts.
304
322
  # @param priority_list [Boolean, nil] when +true+, restricts to issues whose +Priority List+ issue field is
305
323
  # +Yes+ (server-side filter via the +issue_field_values+ query param). +false+ raises +ArgumentError+ -- GitHub
306
324
  # has no negation qualifier. Silently dropped when +config.issue_fields_enabled+ is +false+.
@@ -309,19 +327,38 @@ module PlanMyStuff
309
327
  #
310
328
  # @return [Array<PlanMyStuff::Issue>]
311
329
  #
312
- def list(repo: nil, state: :open, labels: [], priority_list: nil, page: 1, per_page: 25)
330
+ def list(
331
+ repo: nil,
332
+ state: :open,
333
+ labels: [],
334
+ issue_type: nil,
335
+ issue_fields: nil,
336
+ priority_list: nil,
337
+ page: 1,
338
+ per_page: 25
339
+ )
313
340
  if priority_list == false
314
341
  raise(ArgumentError, 'priority_list: false is not supported (no GitHub negation qualifier)')
315
342
  end
343
+ if issue_fields.present? && !PlanMyStuff.configuration.issue_fields_enabled
344
+ raise(PlanMyStuff::IssueFieldsNotEnabledError)
345
+ end
316
346
 
317
347
  client = PlanMyStuff.client
318
348
  resolved_repo = client.resolve_repo!(repo)
319
349
 
320
350
  params = { state: state.to_s, page: page, per_page: per_page }
321
351
  params[:labels] = labels.sort.join(',') if labels.present?
352
+
353
+ resolved_type = resolve_issue_types_filter(issue_type)
354
+ params[:type] = resolved_type if resolved_type.present?
355
+
356
+ field_pairs = []
357
+ field_pairs.concat(build_issue_field_filter_pairs(issue_fields)) if issue_fields.present?
322
358
  if priority_list && PlanMyStuff.configuration.issue_fields_enabled
323
- params[:issue_field_values] = 'priority-list:Yes'
359
+ field_pairs << 'priority-list:Yes'
324
360
  end
361
+ params[:issue_field_values] = field_pairs.join(',') if field_pairs.present?
325
362
 
326
363
  github_issues = client.rest(:list_issues, resolved_repo, **params)
327
364
  filtered = github_issues.reject { |gi| gi.respond_to?(:pull_request) && gi.pull_request }
@@ -347,11 +384,18 @@ module PlanMyStuff
347
384
  # - The Search API has its own rate limit (30 req/min authenticated) separate from
348
385
  # the core REST API.
349
386
  #
350
- # @raise [ArgumentError] when +priority_list: false+ is passed
387
+ # @raise [ArgumentError] when +priority_list: false+ is passed, or when +issue_type:+ is an Array
388
+ # @raise [PlanMyStuff::IssueFieldsNotEnabledError] when +issue_fields:+ is passed and
389
+ # +config.issue_fields_enabled+ is +false+
351
390
  #
352
391
  # @param repo [Symbol, String, nil] defaults to config.default_repo
353
392
  # @param state [Symbol] :open, :closed, or :all
354
393
  # @param labels [Array<String>]
394
+ # @param issue_type [String, Symbol, nil] a single GitHub issue type name. Symbols resolve through
395
+ # +ISSUE_TYPE_NICKNAMES+ then +config.issue_types+.
396
+ # @param issue_fields [Hash{String,Symbol => Object,Range,nil}, nil] GitHub Issue Field equality / range
397
+ # filters. See +.list+ for the value shapes the gem accepts. Each pair is emitted as a
398
+ # +field.<slug>:<value>+ Search qualifier and triggers +advanced_search=true+.
355
399
  # @param priority_list [Boolean, nil] when +true+, restricts to issues whose +Priority List+ issue field is
356
400
  # +Yes+ (server-side filter via the +field.priority-list:Yes+ Search qualifier). +false+ raises
357
401
  # +ArgumentError+ -- GitHub has no negation qualifier. Silently dropped when
@@ -359,10 +403,13 @@ module PlanMyStuff
359
403
  #
360
404
  # @return [Integer]
361
405
  #
362
- def count(repo: nil, state: :open, labels: [], priority_list: nil)
406
+ def count(repo: nil, state: :open, labels: [], issue_type: nil, issue_fields: nil, priority_list: nil)
363
407
  if priority_list == false
364
408
  raise(ArgumentError, 'priority_list: false is not supported (no GitHub negation qualifier)')
365
409
  end
410
+ if issue_fields.present? && !PlanMyStuff.configuration.issue_fields_enabled
411
+ raise(PlanMyStuff::IssueFieldsNotEnabledError)
412
+ end
366
413
 
367
414
  client = PlanMyStuff.client
368
415
  resolved_repo = client.resolve_repo!(repo)
@@ -374,11 +421,19 @@ module PlanMyStuff
374
421
  qualifiers += labels_to_use.map do |label|
375
422
  "label:\"#{label}\""
376
423
  end
377
- search_options = { per_page: 1 }
424
+
425
+ resolved_type = resolve_issue_types_filter(issue_type)
426
+ qualifiers << "type:#{resolved_type}" if resolved_type.present?
427
+
428
+ field_pairs = []
429
+ field_pairs.concat(build_issue_field_filter_pairs(issue_fields)) if issue_fields.present?
378
430
  if priority_list && PlanMyStuff.configuration.issue_fields_enabled
379
- qualifiers << 'field.priority-list:Yes'
380
- search_options[:advanced_search] = true
431
+ field_pairs << 'priority-list:Yes'
381
432
  end
433
+ qualifiers += field_pairs.map { |pair| "field.#{pair}" }
434
+
435
+ search_options = { per_page: 1 }
436
+ search_options[:advanced_search] = true if field_pairs.present?
382
437
  client.rest(:search_issues, qualifiers.join(' '), **search_options).total_count
383
438
  end
384
439
 
@@ -528,6 +583,93 @@ module PlanMyStuff
528
583
  PlanMyStuff.configuration.issue_types[canonical] || canonical
529
584
  end
530
585
 
586
+ # Resolves an +issue_type:+ filter kwarg (used by +.list+ / +.count+) into a single canonical type name.
587
+ # Accepts a scalar (String / Symbol); +nil+ returns +nil+ so callers can skip the filter entirely. Runs
588
+ # through +resolve_issue_type!+ so symbol nicknames and +config.issue_types+ overrides apply consistently
589
+ # with +create!+ / +update!+. Arrays raise +ArgumentError+ -- GitHub's REST +type+ param and Search
590
+ # +type:+ qualifier each accept a single value at a time.
591
+ #
592
+ # @raise [ArgumentError] when +value+ is an Array
593
+ #
594
+ # @param value [String, Symbol, nil]
595
+ #
596
+ # @return [String, nil]
597
+ #
598
+ def resolve_issue_types_filter(value)
599
+ return if value.nil?
600
+ if value.is_a?(Array)
601
+ raise(ArgumentError, 'issue_type: must be a single String / Symbol; GitHub does not accept multiple types')
602
+ end
603
+
604
+ resolve_issue_type!(value)
605
+ end
606
+
607
+ # Slugifies a field name into the kebab-case form GitHub expects in +issue_field_values+ and the Search API's
608
+ # +field.<slug>:+ qualifier (e.g. +"Priority List"+ / +:priority_list+ -> +"priority-list"+). Lowercases and
609
+ # collapses runs of whitespace or underscores into a single hyphen.
610
+ #
611
+ # @param name [String, Symbol]
612
+ #
613
+ # @return [String]
614
+ #
615
+ def field_filter_slug(name)
616
+ name.to_s.downcase.gsub(/[\s_]+/, '-')
617
+ end
618
+
619
+ # Coerces an issue-field filter value to the literal string GitHub expects in the query (Date / Time -> ISO
620
+ # 8601, Numeric / scalar -> +to_s+). Range bounds are handled separately by
621
+ # +build_issue_field_filter_pairs+.
622
+ #
623
+ # @param value [Object]
624
+ #
625
+ # @return [String]
626
+ #
627
+ def format_field_filter_value(value)
628
+ case value
629
+ when Date, Time then value.iso8601
630
+ else value.to_s
631
+ end
632
+ end
633
+
634
+ # Expands an +issue_fields:+ kwarg hash into the flat +Array<String>+ of +slug:value+ pairs that both REST
635
+ # (+issue_field_values=...+) and Search (+field.slug:value...+) consume.
636
+ #
637
+ # Scalars become a single equality pair. +Range+ values expand into one or two comparison pairs:
638
+ # +>=+/+<=+ for inclusive bounds, +<+ for an exclusive end (+...+). Beginless / endless ranges emit only
639
+ # the bounded side. +nil+ values are skipped.
640
+ #
641
+ # @param hash [Hash{String,Symbol => Object,Range,nil}]
642
+ #
643
+ # @return [Array<String>]
644
+ #
645
+ def build_issue_field_filter_pairs(hash)
646
+ hash.flat_map do |name, value|
647
+ slug = field_filter_slug(name)
648
+ case value
649
+ when nil then []
650
+ when Range then range_field_filter_pairs(slug, value)
651
+ else ["#{slug}:#{format_field_filter_value(value)}"]
652
+ end
653
+ end
654
+ end
655
+
656
+ # Expands a +Range+ value into the +slug:>=begin+ / +slug:<=end+ (or +<end+ for +...+) pair(s) GitHub expects.
657
+ #
658
+ # @param slug [String]
659
+ # @param range [Range]
660
+ #
661
+ # @return [Array<String>]
662
+ #
663
+ def range_field_filter_pairs(slug, range)
664
+ pairs = []
665
+ pairs << "#{slug}:>=#{format_field_filter_value(range.begin)}" if range.begin.present?
666
+ if range.end.present?
667
+ end_op = range.exclude_end? ? '<' : '<='
668
+ pairs << "#{slug}:#{end_op}#{format_field_filter_value(range.end)}"
669
+ end
670
+ pairs
671
+ end
672
+
531
673
  # @raise [PlanMyStuff::APIError] when the GitHub API call fails
532
674
  #
533
675
  # @return [Hash]
@@ -43,6 +43,22 @@ module PlanMyStuff
43
43
  user.public_send(PlanMyStuff.configuration.user_id_method)
44
44
  end
45
45
 
46
+ # Inverse lookup of +config.github_login_for+. Returns the user object whose configured GitHub login matches
47
+ # +login+, or +nil+ if +login+ is blank or unmapped.
48
+ #
49
+ # @param login [String, nil] GitHub login (e.g. from a webhook payload)
50
+ #
51
+ # @return [Object, nil]
52
+ #
53
+ def from_github_login(login)
54
+ return if login.blank?
55
+
56
+ user_id, _login = PlanMyStuff.configuration.github_login_for.key(login)
57
+ return if user_id.nil?
58
+
59
+ resolve(user_id)
60
+ end
61
+
46
62
  # Checks whether a user is support staff.
47
63
  # Anonymous users (nil) are never support.
48
64
  #
@@ -3,7 +3,7 @@
3
3
  module PlanMyStuff
4
4
  module VERSION
5
5
  MAJOR = 0
6
- MINOR = 23
6
+ MINOR = 24
7
7
  TINY = 0
8
8
 
9
9
  # Set PRE to nil unless it's a pre-release (beta, rc, etc.)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plan_my_stuff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.23.0
4
+ version: 0.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brands Insurance