saltext.github 1.0.0__py2.py3-none-any.whl

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.
@@ -0,0 +1,2195 @@
1
+ """
2
+ Module for interacting with the GitHub v3 API.
3
+
4
+ .. versionadded:: 2016.3.0
5
+
6
+ :depends: PyGithub python module
7
+
8
+ Configuration
9
+ -------------
10
+
11
+ Configure this module by specifying the name of a configuration
12
+ profile in the minion config, minion pillar, or master config. The module
13
+ will use the 'github' key by default, if defined.
14
+
15
+ For example:
16
+
17
+ .. code-block:: yaml
18
+
19
+ github:
20
+ token: abc1234
21
+ org_name: my_organization
22
+
23
+ # optional: some functions require a repo_name, which
24
+ # can be set in the config file, or passed in at the CLI.
25
+ repo_name: my_repo
26
+
27
+ # optional: it can be dangerous to change the privacy of a repository
28
+ # in an automated way. set this to True to allow privacy modifications
29
+ allow_repo_privacy_changes: False
30
+ """
31
+
32
+ import logging
33
+
34
+ import salt.utils.http
35
+ import salt.utils.json
36
+ from salt.exceptions import CommandExecutionError
37
+
38
+ HAS_LIBS = False
39
+ try:
40
+ # pylint: disable=no-name-in-module
41
+ import github
42
+ import github.NamedUser
43
+ import github.PaginatedList
44
+ from github.GithubException import UnknownObjectException
45
+
46
+ # pylint: enable=no-name-in-module
47
+ HAS_LIBS = True
48
+ except ImportError:
49
+ pass
50
+
51
+ log = logging.getLogger(__name__)
52
+
53
+ __virtualname__ = "github"
54
+
55
+
56
+ def __virtual__():
57
+ """
58
+ Only load this module if PyGithub is installed on this minion.
59
+ """
60
+ if HAS_LIBS:
61
+ return __virtualname__
62
+ return (
63
+ False,
64
+ "The github execution module cannot be loaded: PyGithub library is not installed.",
65
+ )
66
+
67
+
68
+ def _get_config_value(profile, config_name):
69
+ """
70
+ Helper function that returns a profile's configuration value based on
71
+ the supplied configuration name.
72
+
73
+ profile
74
+ The profile name that contains configuration information.
75
+
76
+ config_name
77
+ The configuration item's name to use to return configuration values.
78
+ """
79
+ config = __salt__["config.option"](profile)
80
+ if not config:
81
+ raise CommandExecutionError(
82
+ f"Authentication information could not be found for the '{profile}' profile."
83
+ )
84
+
85
+ config_value = config.get(config_name)
86
+ if config_value is None:
87
+ raise CommandExecutionError(
88
+ f"The '{config_name}' parameter was not found in the '{profile}' profile."
89
+ )
90
+
91
+ return config_value
92
+
93
+
94
+ def _get_client(profile):
95
+ """
96
+ Return the GitHub client, cached into __context__ for performance
97
+ """
98
+ token = _get_config_value(profile, "token")
99
+ key = "github.{}:{}".format( # pylint: disable=consider-using-f-string
100
+ token, _get_config_value(profile, "org_name")
101
+ )
102
+
103
+ if key not in __context__:
104
+ __context__[key] = github.Github(token, per_page=100)
105
+ return __context__[key]
106
+
107
+
108
+ def _get_members(organization, params=None):
109
+ return github.PaginatedList.PaginatedList(
110
+ github.NamedUser.NamedUser,
111
+ organization._requester,
112
+ organization.url + "/members",
113
+ params,
114
+ )
115
+
116
+
117
+ def _get_repos(profile, params=None, ignore_cache=False):
118
+ # Use cache when no params are given
119
+ org_name = _get_config_value(profile, "org_name")
120
+ key = f"github.{org_name}:repos"
121
+
122
+ if key not in __context__ or ignore_cache or params is not None:
123
+ org_name = _get_config_value(profile, "org_name")
124
+ client = _get_client(profile)
125
+ organization = client.get_organization(org_name)
126
+
127
+ result = github.PaginatedList.PaginatedList(
128
+ github.Repository.Repository,
129
+ organization._requester,
130
+ organization.url + "/repos",
131
+ params,
132
+ )
133
+
134
+ # Only cache results if no params were given (full scan)
135
+ if params is not None:
136
+ return result
137
+
138
+ next_result = []
139
+
140
+ for repo in result:
141
+ next_result.append(repo)
142
+
143
+ # Cache a copy of each repo for single lookups
144
+ repo_key = f"github.{org_name}:{repo.name.lower()}:repo_info"
145
+ __context__[repo_key] = _repo_to_dict(repo)
146
+
147
+ __context__[key] = next_result
148
+
149
+ return __context__[key]
150
+
151
+
152
+ def list_users(profile="github", ignore_cache=False):
153
+ """
154
+ List all users within the organization.
155
+
156
+ profile
157
+ The name of the profile configuration to use. Defaults to ``github``.
158
+
159
+ ignore_cache
160
+ Bypasses the use of cached users.
161
+
162
+ .. versionadded:: 2016.11.0
163
+
164
+ CLI Example:
165
+
166
+ .. code-block:: bash
167
+
168
+ salt myminion github.list_users
169
+ salt myminion github.list_users profile='my-github-profile'
170
+ """
171
+ org_name = _get_config_value(profile, "org_name")
172
+ key = f"github.{org_name}:users"
173
+ if key not in __context__ or ignore_cache:
174
+ client = _get_client(profile)
175
+ organization = client.get_organization(org_name)
176
+ __context__[key] = [member.login for member in _get_members(organization, None)]
177
+ return __context__[key]
178
+
179
+
180
+ def get_user(name, profile="github", user_details=False):
181
+ """
182
+ Get a GitHub user by name.
183
+
184
+ name
185
+ The user for which to obtain information.
186
+
187
+ profile
188
+ The name of the profile configuration to use. Defaults to ``github``.
189
+
190
+ user_details
191
+ Prints user information details. Defaults to ``False``. If the user is
192
+ already in the organization and user_details is set to False, the
193
+ get_user function returns ``True``. If the user is not already present
194
+ in the organization, user details will be printed by default.
195
+
196
+ CLI Example:
197
+
198
+ .. code-block:: bash
199
+
200
+ salt myminion github.get_user github-handle
201
+ salt myminion github.get_user github-handle user_details=true
202
+
203
+ """
204
+
205
+ if not user_details and name in list_users(profile):
206
+ # User is in the org, no need for additional Data
207
+ return True
208
+
209
+ response = {}
210
+ client = _get_client(profile)
211
+ organization = client.get_organization(_get_config_value(profile, "org_name"))
212
+
213
+ try:
214
+ user = client.get_user(name)
215
+ except UnknownObjectException:
216
+ log.exception("Resource not found")
217
+ return False
218
+
219
+ response["company"] = user.company
220
+ response["created_at"] = user.created_at
221
+ response["email"] = user.email
222
+ response["html_url"] = user.html_url
223
+ response["id"] = user.id
224
+ response["login"] = user.login
225
+ response["name"] = user.name
226
+ response["type"] = user.type
227
+ response["url"] = user.url
228
+
229
+ try:
230
+ headers, data = organization._requester.requestJsonAndCheck(
231
+ "GET", organization.url + "/memberships/" + user._identity
232
+ )
233
+ except UnknownObjectException:
234
+ response["membership_state"] = "nonexistent"
235
+ response["in_org"] = False
236
+ return response
237
+
238
+ response["in_org"] = organization.has_in_members(user)
239
+ response["membership_state"] = data.get("state")
240
+
241
+ return response
242
+
243
+
244
+ def add_user(name, profile="github"):
245
+ """
246
+ Add a GitHub user.
247
+
248
+ name
249
+ The user for which to obtain information.
250
+
251
+ profile
252
+ The name of the profile configuration to use. Defaults to ``github``.
253
+
254
+ CLI Example:
255
+
256
+ .. code-block:: bash
257
+
258
+ salt myminion github.add_user github-handle
259
+ """
260
+
261
+ client = _get_client(profile)
262
+ organization = client.get_organization(_get_config_value(profile, "org_name"))
263
+
264
+ try:
265
+ github_named_user = client.get_user(name)
266
+ except UnknownObjectException:
267
+ log.exception("Resource not found")
268
+ return False
269
+
270
+ headers, data = organization._requester.requestJsonAndCheck(
271
+ "PUT", organization.url + "/memberships/" + github_named_user._identity
272
+ )
273
+
274
+ return data.get("state") == "pending"
275
+
276
+
277
+ def remove_user(name, profile="github"):
278
+ """
279
+ Remove a Github user by name.
280
+
281
+ name
282
+ The user for which to obtain information.
283
+
284
+ profile
285
+ The name of the profile configuration to use. Defaults to ``github``.
286
+
287
+ CLI Example:
288
+
289
+ .. code-block:: bash
290
+
291
+ salt myminion github.remove_user github-handle
292
+ """
293
+
294
+ client = _get_client(profile)
295
+ organization = client.get_organization(_get_config_value(profile, "org_name"))
296
+
297
+ try:
298
+ git_user = client.get_user(name)
299
+ except UnknownObjectException:
300
+ log.exception("Resource not found")
301
+ return False
302
+
303
+ if organization.has_in_members(git_user):
304
+ organization.remove_from_members(git_user)
305
+
306
+ return not organization.has_in_members(git_user)
307
+
308
+
309
+ def get_issue(issue_number, repo_name=None, profile="github", output="min"):
310
+ """
311
+ Return information about a single issue in a named repository.
312
+
313
+ .. versionadded:: 2016.11.0
314
+
315
+ issue_number
316
+ The number of the issue to retrieve.
317
+
318
+ repo_name
319
+ The name of the repository from which to get the issue. This argument is
320
+ required, either passed via the CLI, or defined in the configured
321
+ profile. A ``repo_name`` passed as a CLI argument will override the
322
+ repo_name defined in the configured profile, if provided.
323
+
324
+ profile
325
+ The name of the profile configuration to use. Defaults to ``github``.
326
+
327
+ output
328
+ The amount of data returned by each issue. Defaults to ``min``. Change
329
+ to ``full`` to see all issue output.
330
+
331
+ CLI Example:
332
+
333
+ .. code-block:: bash
334
+
335
+ salt myminion github.get_issue 514
336
+ salt myminion github.get_issue 514 repo_name=salt
337
+ """
338
+ org_name = _get_config_value(profile, "org_name")
339
+ if repo_name is None:
340
+ repo_name = _get_config_value(profile, "repo_name")
341
+
342
+ action = "/".join(["repos", org_name, repo_name])
343
+ command = "issues/" + str(issue_number)
344
+
345
+ ret = {}
346
+ issue_data = _query(profile, action=action, command=command)
347
+
348
+ issue_id = issue_data.get("dict", {}).get("id")
349
+
350
+ if issue_id:
351
+ if output.lower() == "full":
352
+ ret[issue_id] = issue_data["dict"]
353
+ else:
354
+ ret[issue_id] = _format_issue(issue_data["dict"])
355
+
356
+ return ret
357
+
358
+
359
+ def get_issue_comments(issue_number, repo_name=None, profile="github", since=None, output="min"):
360
+ """
361
+ Return information about the comments for a given issue in a named repository.
362
+
363
+ .. versionadded:: 2016.11.0
364
+
365
+ issue_number
366
+ The number of the issue for which to retrieve comments.
367
+
368
+ repo_name
369
+ The name of the repository to which the issue belongs. This argument is
370
+ required, either passed via the CLI, or defined in the configured
371
+ profile. A ``repo_name`` passed as a CLI argument will override the
372
+ repo_name defined in the configured profile, if provided.
373
+
374
+ profile
375
+ The name of the profile configuration to use. Defaults to ``github``.
376
+
377
+ since
378
+ Only comments updated at or after this time are returned. This is a
379
+ timestamp in ISO 8601 format: ``YYYY-MM-DDTHH:MM:SSZ``.
380
+
381
+ output
382
+ The amount of data returned by each issue. Defaults to ``min``. Change
383
+ to ``full`` to see all issue output.
384
+
385
+ CLI Example:
386
+
387
+ .. code-block:: bash
388
+
389
+ salt myminion github.get_issue_comments 514
390
+ salt myminion github.get_issue 514 repo_name=salt
391
+ """
392
+ org_name = _get_config_value(profile, "org_name")
393
+ if repo_name is None:
394
+ repo_name = _get_config_value(profile, "repo_name")
395
+
396
+ action = "/".join(["repos", org_name, repo_name])
397
+ command = "/".join(["issues", str(issue_number), "comments"])
398
+
399
+ args = {}
400
+ if since:
401
+ args["since"] = since
402
+
403
+ comments = _query(profile, action=action, command=command, args=args)
404
+
405
+ ret = {}
406
+ for comment in comments.get("dict", {}):
407
+ comment_id = comment.get("id")
408
+ if output == "full":
409
+ ret[comment_id] = comment
410
+ else:
411
+ ret[comment_id] = {
412
+ "id": comment.get("id"),
413
+ "created_at": comment.get("created_at"),
414
+ "updated_at": comment.get("updated_at"),
415
+ "user_login": comment.get("user").get("login"),
416
+ }
417
+ return ret
418
+
419
+
420
+ def get_issues(
421
+ repo_name=None,
422
+ profile="github",
423
+ milestone=None,
424
+ state="open",
425
+ assignee=None,
426
+ creator=None,
427
+ mentioned=None,
428
+ labels=None,
429
+ sort="created",
430
+ direction="desc",
431
+ since=None,
432
+ output="min",
433
+ per_page=None,
434
+ ):
435
+ """
436
+ Returns information for all issues in a given repository, based on the search options.
437
+
438
+ .. versionadded:: 2016.11.0
439
+
440
+ repo_name
441
+ The name of the repository for which to list issues. This argument is
442
+ required, either passed via the CLI, or defined in the configured
443
+ profile. A ``repo_name`` passed as a CLI argument will override the
444
+ repo_name defined in the configured profile, if provided.
445
+
446
+ profile
447
+ The name of the profile configuration to use. Defaults to ``github``.
448
+
449
+ milestone
450
+ The number of a GitHub milestone, or a string of either ``*`` or
451
+ ``none``.
452
+
453
+ If a number is passed, it should refer to a milestone by its number
454
+ field. Use the ``github.get_milestone`` function to obtain a milestone's
455
+ number.
456
+
457
+ If the string ``*`` is passed, issues with any milestone are
458
+ accepted. If the string ``none`` is passed, issues without milestones
459
+ are returned.
460
+
461
+ state
462
+ Indicates the state of the issues to return. Can be either ``open``,
463
+ ``closed``, or ``all``. Default is ``open``.
464
+
465
+ assignee
466
+ Can be the name of a user. Pass in ``none`` (as a string) for issues
467
+ with no assigned user or ``*`` for issues assigned to any user.
468
+
469
+ creator
470
+ The user that created the issue.
471
+
472
+ mentioned
473
+ A user that's mentioned in the issue.
474
+
475
+ labels
476
+ A string of comma separated label names. For example, ``bug,ui,@high``.
477
+
478
+ sort
479
+ What to sort results by. Can be either ``created``, ``updated``, or
480
+ ``comments``. Default is ``created``.
481
+
482
+ direction
483
+ The direction of the sort. Can be either ``asc`` or ``desc``. Default
484
+ is ``desc``.
485
+
486
+ since
487
+ Only issues updated at or after this time are returned. This is a
488
+ timestamp in ISO 8601 format: ``YYYY-MM-DDTHH:MM:SSZ``.
489
+
490
+ output
491
+ The amount of data returned by each issue. Defaults to ``min``. Change
492
+ to ``full`` to see all issue output.
493
+
494
+ per_page
495
+ GitHub paginates data in their API calls. Use this value to increase or
496
+ decrease the number of issues gathered from GitHub, per page. If not set,
497
+ GitHub defaults are used. Maximum is 100.
498
+
499
+ CLI Example:
500
+
501
+ .. code-block:: bash
502
+
503
+ salt myminion github.get_issues my-github-repo
504
+ """
505
+ org_name = _get_config_value(profile, "org_name")
506
+ if repo_name is None:
507
+ repo_name = _get_config_value(profile, "repo_name")
508
+
509
+ action = "/".join(["repos", org_name, repo_name])
510
+ args = {}
511
+
512
+ # Build API arguments, as necessary.
513
+ if milestone:
514
+ args["milestone"] = milestone
515
+ if assignee:
516
+ args["assignee"] = assignee
517
+ if creator:
518
+ args["creator"] = creator
519
+ if mentioned:
520
+ args["mentioned"] = mentioned
521
+ if labels:
522
+ args["labels"] = labels
523
+ if since:
524
+ args["since"] = since
525
+ if per_page:
526
+ args["per_page"] = per_page
527
+
528
+ # Only pass the following API args if they're not the defaults listed.
529
+ if state and state != "open":
530
+ args["state"] = state
531
+ if sort and sort != "created":
532
+ args["sort"] = sort
533
+ if direction and direction != "desc":
534
+ args["direction"] = direction
535
+
536
+ ret = {}
537
+ issues = _query(profile, action=action, command="issues", args=args)
538
+
539
+ for issue in issues.get("dict", {}):
540
+ # Pull requests are included in the issue list from GitHub
541
+ # Let's not include those in the return.
542
+ if issue.get("pull_request"):
543
+ continue
544
+ issue_id = issue.get("id")
545
+ if output == "full":
546
+ ret[issue_id] = issue
547
+ else:
548
+ ret[issue_id] = _format_issue(issue)
549
+
550
+ return ret
551
+
552
+
553
+ def get_milestones(
554
+ repo_name=None,
555
+ profile="github",
556
+ state="open",
557
+ sort="due_on",
558
+ direction="asc",
559
+ output="min",
560
+ per_page=None,
561
+ ):
562
+ """
563
+ Return information about milestones for a given repository.
564
+
565
+ .. versionadded:: 2016.11.0
566
+
567
+ repo_name
568
+ The name of the repository for which to list issues. This argument is
569
+ required, either passed via the CLI, or defined in the configured
570
+ profile. A ``repo_name`` passed as a CLI argument will override the
571
+ repo_name defined in the configured profile, if provided.
572
+
573
+ profile
574
+ The name of the profile configuration to use. Defaults to ``github``.
575
+
576
+ state
577
+ The state of the milestone. Either ``open``, ``closed``, or ``all``.
578
+ Default is ``open``.
579
+
580
+ sort
581
+ What to sort results by. Either ``due_on`` or ``completeness``. Default
582
+ is ``due_on``.
583
+
584
+ direction
585
+ The direction of the sort. Either ``asc`` or ``desc``. Default is ``asc``.
586
+
587
+ output
588
+ The amount of data returned by each issue. Defaults to ``min``. Change
589
+ to ``full`` to see all issue output.
590
+
591
+ per_page
592
+ GitHub paginates data in their API calls. Use this value to increase or
593
+ decrease the number of issues gathered from GitHub, per page. If not set,
594
+ GitHub defaults are used.
595
+
596
+ CLI Example:
597
+
598
+ .. code-block:: bash
599
+
600
+ salt myminion github.get_milestones
601
+
602
+ """
603
+ org_name = _get_config_value(profile, "org_name")
604
+ if repo_name is None:
605
+ repo_name = _get_config_value(profile, "repo_name")
606
+
607
+ action = "/".join(["repos", org_name, repo_name])
608
+ args = {}
609
+
610
+ if per_page:
611
+ args["per_page"] = per_page
612
+
613
+ # Only pass the following API args if they're not the defaults listed.
614
+ if state and state != "open":
615
+ args["state"] = state
616
+ if sort and sort != "due_on":
617
+ args["sort"] = sort
618
+ if direction and direction != "asc":
619
+ args["direction"] = direction
620
+
621
+ ret = {}
622
+ milestones = _query(profile, action=action, command="milestones", args=args)
623
+
624
+ for milestone in milestones.get("dict", {}):
625
+ milestone_id = milestone.get("id")
626
+ if output == "full":
627
+ ret[milestone_id] = milestone
628
+ else:
629
+ milestone.pop("creator")
630
+ milestone.pop("html_url")
631
+ milestone.pop("labels_url")
632
+ ret[milestone_id] = milestone
633
+
634
+ return ret
635
+
636
+
637
+ def get_milestone(number=None, name=None, repo_name=None, profile="github", output="min"):
638
+ """
639
+ Return information about a single milestone in a named repository.
640
+
641
+ .. versionadded:: 2016.11.0
642
+
643
+ number
644
+ The number of the milestone to retrieve. If provided, this option
645
+ will be favored over ``name``.
646
+
647
+ name
648
+ The name of the milestone to retrieve.
649
+
650
+ repo_name
651
+ The name of the repository for which to list issues. This argument is
652
+ required, either passed via the CLI, or defined in the configured
653
+ profile. A ``repo_name`` passed as a CLI argument will override the
654
+ repo_name defined in the configured profile, if provided.
655
+
656
+ profile
657
+ The name of the profile configuration to use. Defaults to ``github``.
658
+
659
+ output
660
+ The amount of data returned by each issue. Defaults to ``min``. Change
661
+ to ``full`` to see all issue output.
662
+
663
+ CLI Example:
664
+
665
+ .. code-block:: bash
666
+
667
+ salt myminion github.get_milestone 72
668
+ salt myminion github.get_milestone name=my_milestone
669
+
670
+ """
671
+ ret = {}
672
+
673
+ if not any([number, name]):
674
+ raise CommandExecutionError("Either a milestone 'name' or 'number' must be provided.")
675
+
676
+ org_name = _get_config_value(profile, "org_name")
677
+ if repo_name is None:
678
+ repo_name = _get_config_value(profile, "repo_name")
679
+
680
+ action = "/".join(["repos", org_name, repo_name])
681
+ if number:
682
+ command = "milestones/" + str(number)
683
+ milestone_data = _query(profile, action=action, command=command)
684
+ milestone_data = milestone_data.get("dict", {})
685
+ milestone_id = milestone_data.get("id")
686
+ if milestone_id:
687
+ if output.lower() == "full":
688
+ ret[milestone_id] = milestone_data
689
+ else:
690
+ milestone_data.pop("creator")
691
+ milestone_data.pop("html_url")
692
+ milestone_data.pop("labels_url")
693
+ ret[milestone_id] = milestone_data
694
+ return ret
695
+
696
+ else:
697
+ milestones = get_milestones(repo_name=repo_name, profile=profile, output=output)
698
+ for key, val in milestones.items():
699
+ if val.get("title") == name:
700
+ ret[key] = val
701
+ return ret
702
+
703
+ return ret
704
+
705
+
706
+ def _repo_to_dict(repo):
707
+ ret = {}
708
+ ret["id"] = repo.id
709
+ ret["name"] = repo.name
710
+ ret["full_name"] = repo.full_name
711
+ ret["owner"] = repo.owner.login
712
+ ret["private"] = repo.private
713
+ ret["html_url"] = repo.html_url
714
+ ret["description"] = repo.description
715
+ ret["fork"] = repo.fork
716
+ ret["homepage"] = repo.homepage
717
+ ret["size"] = repo.size
718
+ ret["stargazers_count"] = repo.stargazers_count
719
+ ret["watchers_count"] = repo.watchers_count
720
+ ret["language"] = repo.language
721
+ ret["open_issues_count"] = repo.open_issues_count
722
+ ret["forks"] = repo.forks
723
+ ret["open_issues"] = repo.open_issues
724
+ ret["watchers"] = repo.watchers
725
+ ret["default_branch"] = repo.default_branch
726
+ ret["has_issues"] = repo.has_issues
727
+ ret["has_wiki"] = repo.has_wiki
728
+ ret["has_downloads"] = repo.has_downloads
729
+ return ret
730
+
731
+
732
+ def get_repo_info(repo_name, profile="github", ignore_cache=False):
733
+ """
734
+ Return information for a given repo.
735
+
736
+ .. versionadded:: 2016.11.0
737
+
738
+ repo_name
739
+ The name of the repository.
740
+
741
+ profile
742
+ The name of the profile configuration to use. Defaults to ``github``.
743
+
744
+ CLI Example:
745
+
746
+ .. code-block:: bash
747
+
748
+ salt myminion github.get_repo_info salt
749
+ salt myminion github.get_repo_info salt profile='my-github-profile'
750
+ """
751
+
752
+ org_name = _get_config_value(profile, "org_name")
753
+ key = "github.{}:{}:repo_info".format( # pylint: disable=consider-using-f-string
754
+ _get_config_value(profile, "org_name"), repo_name.lower()
755
+ )
756
+
757
+ if key not in __context__ or ignore_cache:
758
+ client = _get_client(profile)
759
+ try:
760
+ repo = client.get_repo("/".join([org_name, repo_name]))
761
+ if not repo:
762
+ return {}
763
+
764
+ # client.get_repo can return a github.Repository.Repository object,
765
+ # even if the repo is invalid. We need to catch the exception when
766
+ # we try to perform actions on the repo object, rather than above
767
+ # the if statement.
768
+ ret = _repo_to_dict(repo)
769
+
770
+ __context__[key] = ret
771
+ except github.UnknownObjectException as exc:
772
+ raise CommandExecutionError(
773
+ f"The '{repo_name}' repository under the '{org_name}' organization could not be found."
774
+ ) from exc
775
+ return __context__[key]
776
+
777
+
778
+ def get_repo_teams(repo_name, profile="github"):
779
+ """
780
+ Return teams belonging to a repository.
781
+
782
+ .. versionadded:: 2017.7.0
783
+
784
+ repo_name
785
+ The name of the repository from which to retrieve teams.
786
+
787
+ profile
788
+ The name of the profile configuration to use. Defaults to ``github``.
789
+
790
+ CLI Example:
791
+
792
+ .. code-block:: bash
793
+
794
+ salt myminion github.get_repo_teams salt
795
+ salt myminion github.get_repo_teams salt profile='my-github-profile'
796
+ """
797
+ ret = []
798
+ org_name = _get_config_value(profile, "org_name")
799
+ client = _get_client(profile)
800
+
801
+ try:
802
+ repo = client.get_repo("/".join([org_name, repo_name]))
803
+ except github.UnknownObjectException as exc:
804
+ raise CommandExecutionError(
805
+ f"The '{repo_name}' repository under the '{org_name}' organization could not be found."
806
+ ) from exc
807
+ try:
808
+ teams = repo.get_teams()
809
+ for team in teams:
810
+ ret.append({"id": team.id, "name": team.name, "permission": team.permission})
811
+ except github.UnknownObjectException as exc:
812
+ raise CommandExecutionError(
813
+ f"Unable to retrieve teams for repository '{repo_name}' under the '{org_name}' organization."
814
+ ) from exc
815
+ return ret
816
+
817
+
818
+ def list_repos(profile="github"):
819
+ """
820
+ List all repositories within the organization. Includes public and private
821
+ repositories within the organization Dependent upon the access rights of
822
+ the profile token.
823
+
824
+ .. versionadded:: 2016.11.0
825
+
826
+ profile
827
+ The name of the profile configuration to use. Defaults to ``github``.
828
+
829
+ CLI Example:
830
+
831
+ .. code-block:: bash
832
+
833
+ salt myminion github.list_repos
834
+ salt myminion github.list_repos profile='my-github-profile'
835
+ """
836
+ return [repo.name for repo in _get_repos(profile)]
837
+
838
+
839
+ def list_private_repos(profile="github"):
840
+ """
841
+ List private repositories within the organization. Dependent upon the access
842
+ rights of the profile token.
843
+
844
+ .. versionadded:: 2016.11.0
845
+
846
+ profile
847
+ The name of the profile configuration to use. Defaults to ``github``.
848
+
849
+ CLI Example:
850
+
851
+ .. code-block:: bash
852
+
853
+ salt myminion github.list_private_repos
854
+ salt myminion github.list_private_repos profile='my-github-profile'
855
+ """
856
+ repos = []
857
+ for repo in _get_repos(profile):
858
+ if repo.private is True:
859
+ repos.append(repo.name)
860
+ return repos
861
+
862
+
863
+ def list_public_repos(profile="github"):
864
+ """
865
+ List public repositories within the organization.
866
+
867
+ .. versionadded:: 2016.11.0
868
+
869
+ profile
870
+ The name of the profile configuration to use. Defaults to ``github``.
871
+
872
+ CLI Example:
873
+
874
+ .. code-block:: bash
875
+
876
+ salt myminion github.list_public_repos
877
+ salt myminion github.list_public_repos profile='my-github-profile'
878
+ """
879
+ repos = []
880
+ for repo in _get_repos(profile):
881
+ if repo.private is False:
882
+ repos.append(repo.name)
883
+ return repos
884
+
885
+
886
+ def add_repo(
887
+ name,
888
+ description=None,
889
+ homepage=None,
890
+ private=None,
891
+ has_issues=None,
892
+ has_wiki=None,
893
+ has_downloads=None,
894
+ auto_init=None,
895
+ gitignore_template=None,
896
+ license_template=None,
897
+ profile="github",
898
+ ):
899
+ """
900
+ Create a new github repository.
901
+
902
+ name
903
+ The name of the team to be created.
904
+
905
+ description
906
+ The description of the repository.
907
+
908
+ homepage
909
+ The URL with more information about the repository.
910
+
911
+ private
912
+ The visiblity of the repository. Note that private repositories require
913
+ a paid GitHub account.
914
+
915
+ has_issues
916
+ Whether to enable issues for this repository.
917
+
918
+ has_wiki
919
+ Whether to enable the wiki for this repository.
920
+
921
+ has_downloads
922
+ Whether to enable downloads for this repository.
923
+
924
+ auto_init
925
+ Whether to create an initial commit with an empty README.
926
+
927
+ gitignore_template
928
+ The desired language or platform for a .gitignore, e.g "Haskell".
929
+
930
+ license_template
931
+ The desired LICENSE template to apply, e.g "mit" or "mozilla".
932
+
933
+ profile
934
+ The name of the profile configuration to use. Defaults to ``github``.
935
+
936
+ CLI Example:
937
+
938
+ .. code-block:: bash
939
+
940
+ salt myminion github.add_repo 'repo_name'
941
+
942
+ .. versionadded:: 2016.11.0
943
+ """
944
+ try:
945
+ client = _get_client(profile)
946
+ organization = client.get_organization(_get_config_value(profile, "org_name"))
947
+ given_params = {
948
+ "description": description,
949
+ "homepage": homepage,
950
+ "private": private,
951
+ "has_issues": has_issues,
952
+ "has_wiki": has_wiki,
953
+ "has_downloads": has_downloads,
954
+ "auto_init": auto_init,
955
+ "gitignore_template": gitignore_template,
956
+ "license_template": license_template,
957
+ }
958
+ parameters = {"name": name}
959
+ for param_name, param_value in given_params.items():
960
+ if param_value is not None:
961
+ parameters[param_name] = param_value
962
+
963
+ organization._requester.requestJsonAndCheck(
964
+ "POST", organization.url + "/repos", input=parameters
965
+ )
966
+ return True
967
+ except github.GithubException:
968
+ log.exception("Error creating a repo")
969
+ return False
970
+
971
+
972
+ def edit_repo(
973
+ name,
974
+ description=None,
975
+ homepage=None,
976
+ private=None,
977
+ has_issues=None,
978
+ has_wiki=None,
979
+ has_downloads=None,
980
+ profile="github",
981
+ ):
982
+ """
983
+ Updates an existing Github repository.
984
+
985
+ name
986
+ The name of the team to be created.
987
+
988
+ description
989
+ The description of the repository.
990
+
991
+ homepage
992
+ The URL with more information about the repository.
993
+
994
+ private
995
+ The visiblity of the repository. Note that private repositories require
996
+ a paid GitHub account.
997
+
998
+ has_issues
999
+ Whether to enable issues for this repository.
1000
+
1001
+ has_wiki
1002
+ Whether to enable the wiki for this repository.
1003
+
1004
+ has_downloads
1005
+ Whether to enable downloads for this repository.
1006
+
1007
+ profile
1008
+ The name of the profile configuration to use. Defaults to ``github``.
1009
+
1010
+ CLI Example:
1011
+
1012
+ .. code-block:: bash
1013
+
1014
+ salt myminion github.add_repo 'repo_name'
1015
+
1016
+ .. versionadded:: 2016.11.0
1017
+ """
1018
+
1019
+ try:
1020
+ allow_private_change = _get_config_value(profile, "allow_repo_privacy_changes")
1021
+ except CommandExecutionError:
1022
+ allow_private_change = False
1023
+
1024
+ if private is not None and not allow_private_change:
1025
+ raise CommandExecutionError(
1026
+ f"The private field is set to be changed for repo {name} but allow_repo_privacy_changes disallows this."
1027
+ )
1028
+
1029
+ try:
1030
+ client = _get_client(profile)
1031
+ organization = client.get_organization(_get_config_value(profile, "org_name"))
1032
+ repo = organization.get_repo(name)
1033
+
1034
+ given_params = {
1035
+ "description": description,
1036
+ "homepage": homepage,
1037
+ "private": private,
1038
+ "has_issues": has_issues,
1039
+ "has_wiki": has_wiki,
1040
+ "has_downloads": has_downloads,
1041
+ }
1042
+ parameters = {"name": name}
1043
+ for param_name, param_value in given_params.items():
1044
+ if param_value is not None:
1045
+ parameters[param_name] = param_value
1046
+
1047
+ organization._requester.requestJsonAndCheck("PATCH", repo.url, input=parameters)
1048
+ get_repo_info(name, profile=profile, ignore_cache=True) # Refresh cache
1049
+ return True
1050
+ except github.GithubException:
1051
+ log.exception("Error editing a repo")
1052
+ return False
1053
+
1054
+
1055
+ def remove_repo(name, profile="github"):
1056
+ """
1057
+ Remove a Github repository.
1058
+
1059
+ name
1060
+ The name of the repository to be removed.
1061
+
1062
+ profile
1063
+ The name of the profile configuration to use. Defaults to ``github``.
1064
+
1065
+ CLI Example:
1066
+
1067
+ .. code-block:: bash
1068
+
1069
+ salt myminion github.remove_repo 'my-repo'
1070
+
1071
+ .. versionadded:: 2016.11.0
1072
+ """
1073
+ repo_info = get_repo_info(name, profile=profile)
1074
+ if not repo_info:
1075
+ log.error("Repo %s to be removed does not exist.", name)
1076
+ return False
1077
+ try:
1078
+ client = _get_client(profile)
1079
+ organization = client.get_organization(_get_config_value(profile, "org_name"))
1080
+ repo = organization.get_repo(name)
1081
+ repo.delete()
1082
+ _get_repos(profile=profile, ignore_cache=True) # refresh cache
1083
+ return True
1084
+ except github.GithubException:
1085
+ log.exception("Error deleting a repo")
1086
+ return False
1087
+
1088
+
1089
+ def get_team(name, profile="github"):
1090
+ """
1091
+ Returns the team details if a team with the given name exists, or None
1092
+ otherwise.
1093
+
1094
+ name
1095
+ The team name for which to obtain information.
1096
+
1097
+ profile
1098
+ The name of the profile configuration to use. Defaults to ``github``.
1099
+
1100
+ CLI Example:
1101
+
1102
+ .. code-block:: bash
1103
+
1104
+ salt myminion github.get_team 'team_name'
1105
+ """
1106
+ return list_teams(profile).get(name)
1107
+
1108
+
1109
+ def add_team(
1110
+ name,
1111
+ description=None,
1112
+ repo_names=None,
1113
+ privacy=None,
1114
+ permission=None,
1115
+ profile="github",
1116
+ ):
1117
+ """
1118
+ Create a new Github team within an organization.
1119
+
1120
+ name
1121
+ The name of the team to be created.
1122
+
1123
+ description
1124
+ The description of the team.
1125
+
1126
+ repo_names
1127
+ The names of repositories to add the team to.
1128
+
1129
+ privacy
1130
+ The level of privacy for the team, can be 'secret' or 'closed'.
1131
+
1132
+ permission
1133
+ The default permission for new repositories added to the team, can be
1134
+ 'pull', 'push' or 'admin'.
1135
+
1136
+ profile
1137
+ The name of the profile configuration to use. Defaults to ``github``.
1138
+
1139
+ CLI Example:
1140
+
1141
+ .. code-block:: bash
1142
+
1143
+ salt myminion github.add_team 'team_name'
1144
+
1145
+ .. versionadded:: 2016.11.0
1146
+ """
1147
+ try:
1148
+ client = _get_client(profile)
1149
+ organization = client.get_organization(_get_config_value(profile, "org_name"))
1150
+ parameters = {}
1151
+ parameters["name"] = name
1152
+
1153
+ if description is not None:
1154
+ parameters["description"] = description
1155
+ if repo_names is not None:
1156
+ parameters["repo_names"] = repo_names
1157
+ if permission is not None:
1158
+ parameters["permission"] = permission
1159
+ if privacy is not None:
1160
+ parameters["privacy"] = privacy
1161
+
1162
+ organization._requester.requestJsonAndCheck(
1163
+ "POST", organization.url + "/teams", input=parameters
1164
+ )
1165
+ list_teams(ignore_cache=True) # Refresh cache
1166
+ return True
1167
+ except github.GithubException:
1168
+ log.exception("Error creating a team")
1169
+ return False
1170
+
1171
+
1172
+ def edit_team(name, description=None, privacy=None, permission=None, profile="github"):
1173
+ """
1174
+ Updates an existing Github team.
1175
+
1176
+ name
1177
+ The name of the team to be edited.
1178
+
1179
+ description
1180
+ The description of the team.
1181
+
1182
+ privacy
1183
+ The level of privacy for the team, can be 'secret' or 'closed'.
1184
+
1185
+ permission
1186
+ The default permission for new repositories added to the team, can be
1187
+ 'pull', 'push' or 'admin'.
1188
+
1189
+ profile
1190
+ The name of the profile configuration to use. Defaults to ``github``.
1191
+
1192
+ CLI Example:
1193
+
1194
+ .. code-block:: bash
1195
+
1196
+ salt myminion github.edit_team 'team_name' description='Team description'
1197
+
1198
+ .. versionadded:: 2016.11.0
1199
+ """
1200
+ team = get_team(name, profile=profile)
1201
+ if not team:
1202
+ log.error("Team %s does not exist", name)
1203
+ return False
1204
+ try:
1205
+ client = _get_client(profile)
1206
+ organization = client.get_organization(_get_config_value(profile, "org_name"))
1207
+ team = organization.get_team(team["id"])
1208
+
1209
+ parameters = {}
1210
+ if name is not None:
1211
+ parameters["name"] = name
1212
+ if description is not None:
1213
+ parameters["description"] = description
1214
+ if privacy is not None:
1215
+ parameters["privacy"] = privacy
1216
+ if permission is not None:
1217
+ parameters["permission"] = permission
1218
+
1219
+ team._requester.requestJsonAndCheck("PATCH", team.url, input=parameters)
1220
+ return True
1221
+ except UnknownObjectException:
1222
+ log.exception("Resource not found")
1223
+ return False
1224
+
1225
+
1226
+ def remove_team(name, profile="github"):
1227
+ """
1228
+ Remove a github team.
1229
+
1230
+ name
1231
+ The name of the team to be removed.
1232
+
1233
+ profile
1234
+ The name of the profile configuration to use. Defaults to ``github``.
1235
+
1236
+ CLI Example:
1237
+
1238
+ .. code-block:: bash
1239
+
1240
+ salt myminion github.remove_team 'team_name'
1241
+
1242
+ .. versionadded:: 2016.11.0
1243
+ """
1244
+ team_info = get_team(name, profile=profile)
1245
+ if not team_info:
1246
+ log.error("Team %s to be removed does not exist.", name)
1247
+ return False
1248
+ try:
1249
+ client = _get_client(profile)
1250
+ organization = client.get_organization(_get_config_value(profile, "org_name"))
1251
+ team = organization.get_team(team_info["id"])
1252
+ team.delete()
1253
+ return list_teams(ignore_cache=True, profile=profile).get(name) is None
1254
+ except github.GithubException:
1255
+ log.exception("Error deleting a team")
1256
+ return False
1257
+
1258
+
1259
+ def list_team_repos(team_name, profile="github", ignore_cache=False):
1260
+ """
1261
+ Gets the repo details for a given team as a dict from repo_name to repo details.
1262
+ Note that repo names are always in lower case.
1263
+
1264
+ team_name
1265
+ The name of the team from which to list repos.
1266
+
1267
+ profile
1268
+ The name of the profile configuration to use. Defaults to ``github``.
1269
+
1270
+ ignore_cache
1271
+ Bypasses the use of cached team repos.
1272
+
1273
+ CLI Example:
1274
+
1275
+ .. code-block:: bash
1276
+
1277
+ salt myminion github.list_team_repos 'team_name'
1278
+
1279
+ .. versionadded:: 2016.11.0
1280
+ """
1281
+ cached_team = get_team(team_name, profile=profile)
1282
+ if not cached_team:
1283
+ log.error("Team %s does not exist.", team_name)
1284
+ return False
1285
+
1286
+ # Return from cache if available
1287
+ if cached_team.get("repos") and not ignore_cache:
1288
+ return cached_team.get("repos")
1289
+
1290
+ try:
1291
+ client = _get_client(profile)
1292
+ organization = client.get_organization(_get_config_value(profile, "org_name"))
1293
+ team = organization.get_team(cached_team["id"])
1294
+ except UnknownObjectException:
1295
+ log.exception("Resource not found: %s", cached_team["id"])
1296
+ try:
1297
+ repos = {}
1298
+ for repo in team.get_repos():
1299
+ permission = "pull"
1300
+ if repo.permissions.admin:
1301
+ permission = "admin"
1302
+ elif repo.permissions.push:
1303
+ permission = "push"
1304
+
1305
+ repos[repo.name.lower()] = {"permission": permission}
1306
+ cached_team["repos"] = repos
1307
+ return repos
1308
+ except UnknownObjectException:
1309
+ log.exception("Resource not found: %s", cached_team["id"])
1310
+ return []
1311
+
1312
+
1313
+ def add_team_repo(repo_name, team_name, profile="github", permission=None):
1314
+ """
1315
+ Adds a repository to a team with team_name.
1316
+
1317
+ repo_name
1318
+ The name of the repository to add.
1319
+
1320
+ team_name
1321
+ The name of the team of which to add the repository.
1322
+
1323
+ profile
1324
+ The name of the profile configuration to use. Defaults to ``github``.
1325
+
1326
+ permission
1327
+ The permission for team members within the repository, can be 'pull',
1328
+ 'push' or 'admin'. If not specified, the default permission specified on
1329
+ the team will be used.
1330
+
1331
+ .. versionadded:: 2017.7.0
1332
+
1333
+ CLI Example:
1334
+
1335
+ .. code-block:: bash
1336
+
1337
+ salt myminion github.add_team_repo 'my_repo' 'team_name'
1338
+
1339
+ .. versionadded:: 2016.11.0
1340
+ """
1341
+ team = get_team(team_name, profile=profile)
1342
+ if not team:
1343
+ log.error("Team %s does not exist", team_name)
1344
+ return False
1345
+ try:
1346
+ client = _get_client(profile)
1347
+ organization = client.get_organization(_get_config_value(profile, "org_name"))
1348
+ team = organization.get_team(team["id"])
1349
+ repo = organization.get_repo(repo_name)
1350
+ except UnknownObjectException:
1351
+ log.exception("Resource not found: %s", team["id"])
1352
+ return False
1353
+ params = None
1354
+ if permission is not None:
1355
+ params = {"permission": permission}
1356
+
1357
+ headers, data = team._requester.requestJsonAndCheck(
1358
+ "PUT", team.url + "/repos/" + repo._identity, input=params
1359
+ )
1360
+ # Try to refresh cache
1361
+ list_team_repos(team_name, profile=profile, ignore_cache=True)
1362
+ return True
1363
+
1364
+
1365
+ def remove_team_repo(repo_name, team_name, profile="github"):
1366
+ """
1367
+ Removes a repository from a team with team_name.
1368
+
1369
+ repo_name
1370
+ The name of the repository to remove.
1371
+
1372
+ team_name
1373
+ The name of the team of which to remove the repository.
1374
+
1375
+ profile
1376
+ The name of the profile configuration to use. Defaults to ``github``.
1377
+
1378
+ CLI Example:
1379
+
1380
+ .. code-block:: bash
1381
+
1382
+ salt myminion github.remove_team_repo 'my_repo' 'team_name'
1383
+
1384
+ .. versionadded:: 2016.11.0
1385
+ """
1386
+ team = get_team(team_name, profile=profile)
1387
+ if not team:
1388
+ log.error("Team %s does not exist", team_name)
1389
+ return False
1390
+ try:
1391
+ client = _get_client(profile)
1392
+ organization = client.get_organization(_get_config_value(profile, "org_name"))
1393
+ team = organization.get_team(team["id"])
1394
+ repo = organization.get_repo(repo_name)
1395
+ except UnknownObjectException:
1396
+ log.exception("Resource not found: %s", team["id"])
1397
+ return False
1398
+ team.remove_from_repos(repo)
1399
+ return repo_name not in list_team_repos(team_name, profile=profile, ignore_cache=True)
1400
+
1401
+
1402
+ def list_team_members(team_name, profile="github", ignore_cache=False):
1403
+ """
1404
+ Gets the names of team members in lower case.
1405
+
1406
+ team_name
1407
+ The name of the team from which to list members.
1408
+
1409
+ profile
1410
+ The name of the profile configuration to use. Defaults to ``github``.
1411
+
1412
+ ignore_cache
1413
+ Bypasses the use of cached team members.
1414
+
1415
+ CLI Example:
1416
+
1417
+ .. code-block:: bash
1418
+
1419
+ salt myminion github.list_team_members 'team_name'
1420
+
1421
+ .. versionadded:: 2016.11.0
1422
+ """
1423
+ cached_team = get_team(team_name, profile=profile)
1424
+ if not cached_team:
1425
+ log.error("Team %s does not exist.", team_name)
1426
+ return False
1427
+ # Return from cache if available
1428
+ if cached_team.get("members") and not ignore_cache:
1429
+ return cached_team.get("members")
1430
+
1431
+ try:
1432
+ client = _get_client(profile)
1433
+ organization = client.get_organization(_get_config_value(profile, "org_name"))
1434
+ team = organization.get_team(cached_team["id"])
1435
+ except UnknownObjectException:
1436
+ log.exception("Resource not found: %s", cached_team["id"])
1437
+ try:
1438
+ cached_team["members"] = [member.login.lower() for member in team.get_members()]
1439
+ return cached_team["members"]
1440
+ except UnknownObjectException:
1441
+ log.exception("Resource not found: %s", cached_team["id"])
1442
+ return []
1443
+
1444
+
1445
+ def list_members_without_mfa(profile="github", ignore_cache=False):
1446
+ """
1447
+ List all members (in lower case) without MFA turned on.
1448
+
1449
+ profile
1450
+ The name of the profile configuration to use. Defaults to ``github``.
1451
+
1452
+ ignore_cache
1453
+ Bypasses the use of cached team repos.
1454
+
1455
+ CLI Example:
1456
+
1457
+ .. code-block:: bash
1458
+
1459
+ salt myminion github.list_members_without_mfa
1460
+
1461
+ .. versionadded:: 2016.11.0
1462
+ """
1463
+ key = "github.{}:non_mfa_users".format( # pylint: disable=consider-using-f-string
1464
+ _get_config_value(profile, "org_name")
1465
+ )
1466
+
1467
+ if key not in __context__ or ignore_cache:
1468
+ client = _get_client(profile)
1469
+ organization = client.get_organization(_get_config_value(profile, "org_name"))
1470
+
1471
+ filter_key = "filter"
1472
+ # Silly hack to see if we're past PyGithub 1.26.0, where the name of
1473
+ # the filter kwarg changed
1474
+ if hasattr(github.Team.Team, "membership"):
1475
+ filter_key = "filter_"
1476
+
1477
+ __context__[key] = [
1478
+ m.login.lower() for m in _get_members(organization, {filter_key: "2fa_disabled"})
1479
+ ]
1480
+ return __context__[key]
1481
+
1482
+
1483
+ def is_team_member(name, team_name, profile="github"):
1484
+ """
1485
+ Returns True if the github user is in the team with team_name, or False
1486
+ otherwise.
1487
+
1488
+ name
1489
+ The name of the user whose membership to check.
1490
+
1491
+ team_name
1492
+ The name of the team to check membership in.
1493
+
1494
+ profile
1495
+ The name of the profile configuration to use. Defaults to ``github``.
1496
+
1497
+ CLI Example:
1498
+
1499
+ .. code-block:: bash
1500
+
1501
+ salt myminion github.is_team_member 'user_name' 'team_name'
1502
+
1503
+ .. versionadded:: 2016.11.0
1504
+ """
1505
+ return name.lower() in list_team_members(team_name, profile=profile)
1506
+
1507
+
1508
+ def add_team_member(name, team_name, profile="github"):
1509
+ """
1510
+ Adds a team member to a team with team_name.
1511
+
1512
+ name
1513
+ The name of the team member to add.
1514
+
1515
+ team_name
1516
+ The name of the team of which to add the user.
1517
+
1518
+ profile
1519
+ The name of the profile configuration to use. Defaults to ``github``.
1520
+
1521
+ CLI Example:
1522
+
1523
+ .. code-block:: bash
1524
+
1525
+ salt myminion github.add_team_member 'user_name' 'team_name'
1526
+
1527
+ .. versionadded:: 2016.11.0
1528
+ """
1529
+ team = get_team(team_name, profile=profile)
1530
+ if not team:
1531
+ log.error("Team %s does not exist", team_name)
1532
+ return False
1533
+ try:
1534
+ client = _get_client(profile)
1535
+ organization = client.get_organization(_get_config_value(profile, "org_name"))
1536
+ team = organization.get_team(team["id"])
1537
+ member = client.get_user(name)
1538
+ except UnknownObjectException:
1539
+ log.exception("Resource not found: %s", team["id"])
1540
+ return False
1541
+
1542
+ try:
1543
+ # Can't use team.add_membership due to this bug that hasn't made it into
1544
+ # a PyGithub release yet https://github.com/PyGithub/PyGithub/issues/363
1545
+ headers, data = team._requester.requestJsonAndCheck(
1546
+ "PUT",
1547
+ team.url + "/memberships/" + member._identity,
1548
+ input={"role": "member"},
1549
+ parameters={"role": "member"},
1550
+ )
1551
+ except github.GithubException:
1552
+ log.exception("Error in adding a member to a team")
1553
+ return False
1554
+ return True
1555
+
1556
+
1557
+ def remove_team_member(name, team_name, profile="github"):
1558
+ """
1559
+ Removes a team member from a team with team_name.
1560
+
1561
+ name
1562
+ The name of the team member to remove.
1563
+
1564
+ team_name
1565
+ The name of the team from which to remove the user.
1566
+
1567
+ profile
1568
+ The name of the profile configuration to use. Defaults to ``github``.
1569
+
1570
+ CLI Example:
1571
+
1572
+ .. code-block:: bash
1573
+
1574
+ salt myminion github.remove_team_member 'user_name' 'team_name'
1575
+
1576
+ .. versionadded:: 2016.11.0
1577
+ """
1578
+ team = get_team(team_name, profile=profile)
1579
+ if not team:
1580
+ log.error("Team %s does not exist", team_name)
1581
+ return False
1582
+ try:
1583
+ client = _get_client(profile)
1584
+ organization = client.get_organization(_get_config_value(profile, "org_name"))
1585
+ team = organization.get_team(team["id"])
1586
+ member = client.get_user(name)
1587
+
1588
+ except UnknownObjectException:
1589
+ log.exception("Resource not found: %s", team["id"])
1590
+ return False
1591
+
1592
+ if not hasattr(team, "remove_from_members"):
1593
+ return (
1594
+ False,
1595
+ "PyGithub 1.26.0 or greater is required for team management, please upgrade.",
1596
+ )
1597
+
1598
+ team.remove_from_members(member)
1599
+ return not team.has_in_members(member)
1600
+
1601
+
1602
+ def list_teams(profile="github", ignore_cache=False):
1603
+ """
1604
+ Lists all teams with the organization.
1605
+
1606
+ profile
1607
+ The name of the profile configuration to use. Defaults to ``github``.
1608
+
1609
+ ignore_cache
1610
+ Bypasses the use of cached teams.
1611
+
1612
+ CLI Example:
1613
+
1614
+ .. code-block:: bash
1615
+
1616
+ salt myminion github.list_teams
1617
+
1618
+ .. versionadded:: 2016.11.0
1619
+ """
1620
+ key = "github.{}:teams".format( # pylint: disable=consider-using-f-string
1621
+ _get_config_value(profile, "org_name")
1622
+ )
1623
+
1624
+ if key not in __context__ or ignore_cache:
1625
+ client = _get_client(profile)
1626
+ organization = client.get_organization(_get_config_value(profile, "org_name"))
1627
+ teams_data = organization.get_teams()
1628
+ teams = {}
1629
+ for team in teams_data:
1630
+ # Note that _rawData is used to access some properties here as they
1631
+ # are not exposed in older versions of PyGithub. It's VERY important
1632
+ # to use team._rawData instead of team.raw_data, as the latter forces
1633
+ # an API call to retrieve team details again.
1634
+ teams[team.name] = {
1635
+ "id": team.id,
1636
+ "slug": team.slug,
1637
+ "description": team._rawData["description"],
1638
+ "permission": team.permission,
1639
+ "privacy": team._rawData["privacy"],
1640
+ }
1641
+ __context__[key] = teams
1642
+
1643
+ return __context__[key]
1644
+
1645
+
1646
+ def get_prs(
1647
+ repo_name=None,
1648
+ profile="github",
1649
+ state="open",
1650
+ head=None,
1651
+ base=None,
1652
+ sort="created",
1653
+ direction="desc",
1654
+ output="min",
1655
+ per_page=None,
1656
+ ):
1657
+ """
1658
+ Returns information for all pull requests in a given repository, based on
1659
+ the search options provided.
1660
+
1661
+ .. versionadded:: 2017.7.0
1662
+
1663
+ repo_name
1664
+ The name of the repository for which to list pull requests. This
1665
+ argument is required, either passed via the CLI, or defined in the
1666
+ configured profile. A ``repo_name`` passed as a CLI argument will
1667
+ override the ``repo_name`` defined in the configured profile, if
1668
+ provided.
1669
+
1670
+ profile
1671
+ The name of the profile configuration to use. Defaults to ``github``.
1672
+
1673
+ state
1674
+ Indicates the state of the pull requests to return. Can be either
1675
+ ``open``, ``closed``, or ``all``. Default is ``open``.
1676
+
1677
+ head
1678
+ Filter pull requests by head user and branch name in the format of
1679
+ ``user:ref-name``. Example: ``'github:new-script-format'``. Default
1680
+ is ``None``.
1681
+
1682
+ base
1683
+ Filter pulls by base branch name. Example: ``gh-pages``. Default is
1684
+ ``None``.
1685
+
1686
+ sort
1687
+ What to sort results by. Can be either ``created``, ``updated``,
1688
+ ``popularity`` (comment count), or ``long-running`` (age, filtering
1689
+ by pull requests updated within the last month). Default is ``created``.
1690
+
1691
+ direction
1692
+ The direction of the sort. Can be either ``asc`` or ``desc``. Default
1693
+ is ``desc``.
1694
+
1695
+ output
1696
+ The amount of data returned by each pull request. Defaults to ``min``.
1697
+ Change to ``full`` to see all pull request output.
1698
+
1699
+ per_page
1700
+ GitHub paginates data in their API calls. Use this value to increase or
1701
+ decrease the number of pull requests gathered from GitHub, per page. If
1702
+ not set, GitHub defaults are used. Maximum is 100.
1703
+
1704
+ CLI Example:
1705
+
1706
+ .. code-block:: bash
1707
+
1708
+ salt myminion github.get_prs
1709
+ salt myminion github.get_prs base=2016.11
1710
+ """
1711
+ org_name = _get_config_value(profile, "org_name")
1712
+ if repo_name is None:
1713
+ repo_name = _get_config_value(profile, "repo_name")
1714
+
1715
+ action = "/".join(["repos", org_name, repo_name])
1716
+ args = {}
1717
+
1718
+ # Build API arguments, as necessary.
1719
+ if head:
1720
+ args["head"] = head
1721
+ if base:
1722
+ args["base"] = base
1723
+ if per_page:
1724
+ args["per_page"] = per_page
1725
+
1726
+ # Only pass the following API args if they're not the defaults listed.
1727
+ if state and state != "open":
1728
+ args["state"] = state
1729
+ if sort and sort != "created":
1730
+ args["sort"] = sort
1731
+ if direction and direction != "desc":
1732
+ args["direction"] = direction
1733
+
1734
+ ret = {}
1735
+ prs = _query(profile, action=action, command="pulls", args=args)
1736
+
1737
+ for pr_ in prs.get("dict", {}):
1738
+ pr_id = pr_.get("id")
1739
+ if output == "full":
1740
+ ret[pr_id] = pr_
1741
+ else:
1742
+ ret[pr_id] = _format_pr(pr_)
1743
+
1744
+ return ret
1745
+
1746
+
1747
+ def _format_pr(pr_):
1748
+ """
1749
+ Helper function to format API return information into a more manageable
1750
+ and useful dictionary for pull request information.
1751
+
1752
+ pr_
1753
+ The pull request to format.
1754
+ """
1755
+ ret = {
1756
+ "id": pr_.get("id"),
1757
+ "pr_number": pr_.get("number"),
1758
+ "state": pr_.get("state"),
1759
+ "title": pr_.get("title"),
1760
+ "user": pr_.get("user").get("login"),
1761
+ "html_url": pr_.get("html_url"),
1762
+ "base_branch": pr_.get("base").get("ref"),
1763
+ }
1764
+
1765
+ return ret
1766
+
1767
+
1768
+ def _format_issue(issue):
1769
+ """
1770
+ Helper function to format API return information into a more manageable
1771
+ and useful dictionary for issue information.
1772
+
1773
+ issue
1774
+ The issue to format.
1775
+ """
1776
+ ret = {
1777
+ "id": issue.get("id"),
1778
+ "issue_number": issue.get("number"),
1779
+ "state": issue.get("state"),
1780
+ "title": issue.get("title"),
1781
+ "user": issue.get("user").get("login"),
1782
+ "html_url": issue.get("html_url"),
1783
+ }
1784
+
1785
+ assignee = issue.get("assignee")
1786
+ if assignee:
1787
+ assignee = assignee.get("login")
1788
+
1789
+ labels = issue.get("labels")
1790
+ label_names = []
1791
+ for label in labels:
1792
+ label_names.append(label.get("name"))
1793
+
1794
+ milestone = issue.get("milestone")
1795
+ if milestone:
1796
+ milestone = milestone.get("title")
1797
+
1798
+ ret["assignee"] = assignee
1799
+ ret["labels"] = label_names
1800
+ ret["milestone"] = milestone
1801
+
1802
+ return ret
1803
+
1804
+
1805
+ def _query(
1806
+ profile,
1807
+ action=None,
1808
+ command=None,
1809
+ args=None,
1810
+ method="GET",
1811
+ header_dict=None,
1812
+ data=None,
1813
+ url="https://api.github.com/",
1814
+ per_page=None,
1815
+ ):
1816
+ """
1817
+ Make a web call to the GitHub API and deal with paginated results.
1818
+ """
1819
+ if not isinstance(args, dict):
1820
+ args = {}
1821
+
1822
+ if action:
1823
+ url += action
1824
+
1825
+ if command:
1826
+ url += f"/{command}"
1827
+
1828
+ log.debug("GitHub URL: %s", url)
1829
+
1830
+ if header_dict is None:
1831
+ header_dict = {}
1832
+
1833
+ if not header_dict.get("Authorization"):
1834
+ header_dict["Authorization"] = f"Bearer {_get_config_value(profile, 'token')}"
1835
+ if per_page and "per_page" not in args.keys():
1836
+ args["per_page"] = per_page
1837
+
1838
+ header_dict["Accept"] = "application/vnd.github+json"
1839
+
1840
+ decode = True
1841
+ if method == "DELETE":
1842
+ decode = False
1843
+
1844
+ # GitHub paginates all queries when returning many items.
1845
+ # Gather all data using multiple queries and handle pagination.
1846
+ ret = {}
1847
+ complete_result = []
1848
+ next_page = True
1849
+ page_number = ""
1850
+ while next_page is True:
1851
+ if page_number:
1852
+ args["page"] = page_number
1853
+ result = salt.utils.http.query(
1854
+ url,
1855
+ method,
1856
+ params=args,
1857
+ data=data,
1858
+ header_dict=header_dict,
1859
+ decode=decode,
1860
+ decode_type="json",
1861
+ headers=True,
1862
+ status=True,
1863
+ text=True,
1864
+ hide_fields=None,
1865
+ opts=__opts__,
1866
+ )
1867
+ log.debug("GitHub Response Status Code: %s", result.get("status"))
1868
+
1869
+ if result.get("status") in [200, 201]:
1870
+
1871
+ if isinstance(result["dict"], dict):
1872
+ # If only querying for one item, such as a single issue
1873
+ # The GitHub API returns a single dictionary, instead of
1874
+ # A list of dictionaries. In that case, we can return.
1875
+ return result
1876
+
1877
+ ret = result
1878
+ complete_result = complete_result + result["dict"]
1879
+ ret["dict"] = complete_result
1880
+
1881
+ else:
1882
+ if result:
1883
+ return result
1884
+ raise CommandExecutionError
1885
+
1886
+ try:
1887
+ link_info = result.get("headers").get("Link").split(",")[0]
1888
+ except AttributeError:
1889
+ # Only one page of data was returned; exit the loop.
1890
+ next_page = False
1891
+ continue
1892
+
1893
+ if "next" in link_info:
1894
+ # Get the 'next' page number from the Link header.
1895
+ page_number = link_info.split(">")[0].split("&page=")[1]
1896
+ else:
1897
+ # Last page already processed; break the loop.
1898
+ next_page = False
1899
+
1900
+ return ret
1901
+
1902
+
1903
+ def _check_ruleset_params(profile, rule_id=False, rule_params=False, **kwargs):
1904
+ """
1905
+ Helper function to check for param values on command line and in config file
1906
+
1907
+ profile
1908
+ The name of the profile configuration to use.
1909
+
1910
+ ruleset_id
1911
+ Defautls to false. If ruleset_id is required, set to True.
1912
+
1913
+ ruleset_params
1914
+ Defautls to false. If ruleset_params are required, set to True.
1915
+
1916
+ """
1917
+ if not kwargs.get("ruleset_type"):
1918
+ kwargs["ruleset_type"] = _get_config_value(profile, "ruleset_type")
1919
+ if kwargs["ruleset_type"] not in ["repo", "org"]:
1920
+ raise CommandExecutionError
1921
+
1922
+ if kwargs["ruleset_type"] == "repo":
1923
+ if not kwargs.get("owner"):
1924
+ kwargs["owner"] = _get_config_value(profile, "owner")
1925
+ if not kwargs.get("repo_name"):
1926
+ kwargs["repo_name"] = _get_config_value(profile, "repo_name")
1927
+
1928
+ action = "/".join(["repos", kwargs["owner"], kwargs["repo_name"], "rulesets"])
1929
+ else:
1930
+ if not kwargs.get("org_name"):
1931
+ kwargs["org_name"] = _get_config_value(profile, "org_name")
1932
+
1933
+ action = "/".join(["orgs", kwargs["org_name"], "rulesets"])
1934
+
1935
+ # check for optional header_dict
1936
+ if not kwargs.get("header_dict"):
1937
+ try:
1938
+ kwargs["header_dict"] = _get_config_value(profile, "header_dict")
1939
+ except CommandExecutionError:
1940
+ kwargs["header_dict"] = {}
1941
+
1942
+ # verify header_dict is a dict
1943
+ if not isinstance(kwargs["header_dict"], dict):
1944
+ kwargs["header_dict"] = {}
1945
+
1946
+ # if ruleset is required, check for ruleset_id
1947
+ if rule_id:
1948
+ if not kwargs.get("ruleset_id"):
1949
+ kwargs["ruleset_id"] = _get_config_value(profile, "ruleset_id")
1950
+
1951
+ action = action + "/" + str(kwargs["ruleset_id"])
1952
+
1953
+ # if ruleset_params are required, check for ruleset_params
1954
+ if rule_params:
1955
+ if not kwargs.get("ruleset_params"):
1956
+ kwargs["ruleset_params"] = _get_config_value(profile, "ruleset_params")
1957
+
1958
+ return kwargs, action
1959
+
1960
+
1961
+ def get_ruleset(profile="github", **kwargs):
1962
+ """
1963
+ Get information of specific ruleset.
1964
+
1965
+ profile
1966
+ The name of the profile configuration to use. Defaults to ``github``.
1967
+ ruleset_type
1968
+ The type of ruleset. 'repo' or 'org'.
1969
+ ruleset_id
1970
+ Id of the ruleset.
1971
+ owner
1972
+ The account owner of the repository.
1973
+ repo_name
1974
+ The name of the repo.
1975
+ org_name
1976
+ The name of the organization.
1977
+ header_dict
1978
+ Headers to pass to Github API.
1979
+
1980
+ CLI Example:
1981
+
1982
+ .. code-block:: bash
1983
+
1984
+ salt myminion github.get_ruleset
1985
+ salt myminion github.get_ruleset ruleset_id=1
1986
+ """
1987
+ params, action = _check_ruleset_params(profile, rule_id=True, **kwargs)
1988
+
1989
+ try:
1990
+ ret = _query(profile, action, header_dict=params["header_dict"])
1991
+ if not ret.get("error"):
1992
+ return ret["dict"]
1993
+ ret["comment"] = f"GitHub Response Status Code: {ret.get('error')}"
1994
+ ret["result"] = False
1995
+ except CommandExecutionError:
1996
+ ret = {}
1997
+ ret["result"] = False
1998
+ ret["comment"] = "Error getting ruleset"
1999
+
2000
+ return ret
2001
+
2002
+
2003
+ def delete_ruleset(profile="github", **kwargs):
2004
+ """
2005
+ Delete ruleset
2006
+
2007
+ profile
2008
+ The name of the profile configuration to use. Defaults to ``github``.
2009
+ ruleset_type
2010
+ The type of ruleset. 'repo' or 'org'.
2011
+ owner
2012
+ The account owner of the repository.
2013
+ repo_name
2014
+ The name of the repo.
2015
+ ruleset_id
2016
+ Id of the ruleset.
2017
+ org_name
2018
+ The name of the organization.
2019
+ header_dict
2020
+ Headers to pass to Github API.
2021
+
2022
+ CLI Example:
2023
+
2024
+ .. code-block:: bash
2025
+
2026
+ salt myminion github.delete_ruleset
2027
+ salt myminion github.delete_ruleset ruleset_id=1
2028
+ """
2029
+ params, action = _check_ruleset_params(profile, rule_id=True, **kwargs)
2030
+
2031
+ try:
2032
+ ret = _query(profile, action, method="DELETE", header_dict=params["header_dict"])
2033
+ if ret.get("error"):
2034
+ ret["comment"] = f"GitHub Response Status Code: {ret.get('error')}"
2035
+ ret["result"] = False
2036
+ else:
2037
+ ret["comment"] = f"ruleset {params['ruleset_id']} successfully deleted"
2038
+ except CommandExecutionError:
2039
+ ret = {}
2040
+ ret["result"] = False
2041
+ ret["comment"] = "Error deleting ruleset"
2042
+ return ret
2043
+
2044
+
2045
+ def update_ruleset(profile="github", **kwargs):
2046
+ """
2047
+ Update ruleset
2048
+
2049
+ profile
2050
+ The name of the profile configuration to use. Defaults to ``github``.
2051
+ ruleset_type
2052
+ The type of ruleset. 'repo' or 'org'.
2053
+ ruleset_id
2054
+ Id of the ruleset.
2055
+ owner
2056
+ The account owner of the repository.
2057
+ repo_name
2058
+ The name of the repo.
2059
+ org_name
2060
+ The name of the organization.
2061
+ header_dict
2062
+ headers to pass to Github API.
2063
+ ruleset_params
2064
+ number of returned results (max 100).
2065
+
2066
+ CLI Example:
2067
+
2068
+ .. code-block:: bash
2069
+
2070
+ salt myminion github.update_ruleset
2071
+ salt myminion github.update_ruleset ruleset_id=1
2072
+ """
2073
+ params, action = _check_ruleset_params(profile, rule_id=True, rule_params=True, **kwargs)
2074
+
2075
+ if not isinstance(params["ruleset_params"], dict):
2076
+ raise CommandExecutionError("params need to be a dict")
2077
+
2078
+ ruleset_params = salt.utils.json.dumps(params["ruleset_params"])
2079
+ try:
2080
+ ret = _query(
2081
+ profile, action, method="PUT", data=ruleset_params, header_dict=params["header_dict"]
2082
+ )
2083
+ if not ret.get("error"):
2084
+ return ret["dict"]
2085
+ else:
2086
+ ret["comment"] = f"GitHub Response Status Code: {ret.get('error')}"
2087
+ ret["result"] = False
2088
+ except CommandExecutionError:
2089
+ ret = {}
2090
+ ret["result"] = False
2091
+ ret["comment"] = "Error updating ruleset"
2092
+
2093
+ return ret
2094
+
2095
+
2096
+ def add_ruleset(profile="github", **kwargs):
2097
+ """
2098
+ Add ruleset
2099
+
2100
+ profile
2101
+ The name of the profile configuration to use. Defaults to ``github``.
2102
+ ruleset_type
2103
+ The type of ruleset. 'repo' or 'org'.
2104
+ owner
2105
+ The account owner of the repository.
2106
+ repo_name
2107
+ The name of the repo.
2108
+ org_name
2109
+ The name of the organization.
2110
+ header_dict
2111
+ headers to pass to Github API.
2112
+ ruleset_params
2113
+ number of returned results (max 100).
2114
+
2115
+ CLI Example:
2116
+
2117
+ .. code-block:: bash
2118
+
2119
+ salt myminion github.add_ruleset
2120
+ """
2121
+ params, action = _check_ruleset_params(profile, rule_params=True, **kwargs)
2122
+
2123
+ if not isinstance(params["ruleset_params"], dict):
2124
+ raise CommandExecutionError("params need to be a dict")
2125
+ if "name" not in params["ruleset_params"]:
2126
+ raise CommandExecutionError("name is required")
2127
+ if "enforcement" not in params["ruleset_params"]:
2128
+ raise CommandExecutionError("enforcement is required")
2129
+
2130
+ ruleset_params = salt.utils.json.dumps(params["ruleset_params"])
2131
+ try:
2132
+ ret = _query(
2133
+ profile, action, method="POST", data=ruleset_params, header_dict=params["header_dict"]
2134
+ )
2135
+ if not ret.get("error"):
2136
+ return ret["dict"]
2137
+ else:
2138
+ ret["comment"] = f"GitHub Response Status Code: {ret.get('error')}"
2139
+ ret["result"] = False
2140
+ except CommandExecutionError:
2141
+ ret = {}
2142
+ ret["result"] = False
2143
+ ret["comment"] = "Error adding ruleset"
2144
+ return ret
2145
+
2146
+
2147
+ def list_rulesets(profile="github", **kwargs):
2148
+ """
2149
+ List rulesets
2150
+
2151
+ profile
2152
+ The name of the profile configuration to use. Defaults to ``github``.
2153
+ ruleset_type
2154
+ The type of ruleset. 'repo' or 'org'.
2155
+ owner
2156
+ The account owner of the repository.
2157
+ repo_name
2158
+ The name of the repo.
2159
+ org_name
2160
+ The name of the organization.
2161
+ header_dict
2162
+ headers to pass to Github API.
2163
+ per_page
2164
+ number of returned results (max 100).
2165
+
2166
+ CLI Example:
2167
+
2168
+ .. code-block:: bash
2169
+
2170
+ salt myminion github.list_rulesets
2171
+ """
2172
+ params, action = _check_ruleset_params(profile, **kwargs)
2173
+
2174
+ if not kwargs.get("per_page"):
2175
+ try:
2176
+ kwargs["per_page"] = _get_config_value(profile, "per_page")
2177
+ except CommandExecutionError:
2178
+ kwargs["per_page"] = None
2179
+
2180
+ try:
2181
+ ret = _query(
2182
+ profile, action, header_dict=params["header_dict"], per_page=kwargs["per_page"]
2183
+ )
2184
+ if not ret.get("error"):
2185
+ if ret["dict"]:
2186
+ return {"rulesets": ret["dict"]}
2187
+ return {"rulesets": None}
2188
+ else:
2189
+ ret["comment"] = f"GitHub Response Status Code: {ret.get('error')}"
2190
+ ret["result"] = False
2191
+ except CommandExecutionError:
2192
+ ret = {}
2193
+ ret["result"] = False
2194
+ ret["comment"] = "Error getting rulesets"
2195
+ return ret