Skip to content

github

This module contains GitHub related functionality used when creating ssb-projects.

create_github(github_token, repo_name, repo_privacy, repo_description, github_org_name)

Creates a GitHub repository with name, description and privacy setting.

Parameters:

Name Type Description Default
github_token str

GitHub personal access token

required
repo_name str

Repository name

required
repo_privacy str

Repository privacy setting, see RepoPrivacy for more information

required
repo_description str

Repository description

required
github_org_name str

Name of GitHub organization

required

Returns:

Name Type Description
str str

Repository url

Source code in ssb_project_cli/ssb_project/create/github.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def create_github(
    github_token: str,
    repo_name: str,
    repo_privacy: str,
    repo_description: str,
    github_org_name: str,
) -> str:
    """Creates a GitHub repository with name, description and privacy setting.

    Args:
        github_token: GitHub personal access token
        repo_name: Repository name
        repo_privacy: Repository privacy setting, see RepoPrivacy for more information
        repo_description: Repository description
        github_org_name: Name of GitHub organization

    Returns:
        str: Repository url
    """
    g = get_environment_specific_github_object(github_token)

    try:
        # Ignoring mypy warning: Unexpected keyword argument "visibility"
        # for "create_repo" of "Organization"  [call-arg]
        g.get_organization(github_org_name).create_repo(
            repo_name,
            visibility=repo_privacy,
            auto_init=False,
            description=repo_description,
        )
    except BadCredentialsException:
        print("Error: Invalid Github credentials")
        create_error_log(
            "".join(format_exc()),
            "create_github",
        )
        exit(1)

    repo = g.get_repo(f"{github_org_name}/{repo_name}")
    repo.replace_topics(["ssb-project"])

    return repo.clone_url

get_environment_specific_github_object(github_token)

Creates and returns a Github object with appropriate settings based on the environment.

Parameters:

Name Type Description Default
github_token str

A personal access token for authenticating with the GitHub API.

required

Returns:

Type Description
Github

A Github object that can be used to interact with the GitHub API.

This function creates a Github object that is specific to the current environment. If the function is running in the onprem environment, SSL verification uses /etc/ssl/certs/ca-certificates.crt. Otherwise, SSL verification is enabled.

Source code in ssb_project_cli/ssb_project/create/github.py
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
def get_environment_specific_github_object(github_token: str) -> Github:
    """Creates and returns a `Github` object with appropriate settings based on the environment.

    Args:
        github_token: A personal access token for authenticating with the GitHub API.

    Returns:
        A `Github` object that can be used to interact with the GitHub API.

    This function creates a `Github` object that is specific to the current environment.
    If the function is running in the onprem environment, SSL verification uses /etc/ssl/certs/ca-certificates.crt.
    Otherwise, SSL verification is enabled.
    """
    if running_onprem(JUPYTER_IMAGE_SPEC):
        # CA bundle to use, supplying this fixes the onprem error "CERTIFICATE_VERIFY_FAILED"
        # verify can be boolean or string, we have to type ignore because mypy expects it to be a bool
        return Github(github_token, verify="/etc/ssl/certs/ca-certificates.crt")
    else:
        return Github(github_token)

get_github_pat(path)

Gets GitHub users and PAT from .gitconfig and .netrc.

Parameters:

Name Type Description Default
path Path

Path to folder containing GitHub credentials.

required

Returns:

Type Description
dict[str, str]

dict[str, str]: A dict with user as key and PAT as value.

Source code in ssb_project_cli/ssb_project/create/github.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def get_github_pat(path: Path) -> dict[str, str]:
    """Gets GitHub users and PAT from .gitconfig and .netrc.

    Args:
        path: Path to folder containing GitHub credentials.

    Returns:
        dict[str, str]: A dict with user as key and PAT as value.
    """
    user_token_dict = get_github_pat_from_gitcredentials(
        path
    ) | get_github_pat_from_netrc(path)

    if not user_token_dict:
        print(
            "Could not find your github token. Add it manually with the --github-token <TOKEN> option\n or fix it by following this guide: https://manual.dapla.ssb.no/git-github.html#sec-pat"
        )
        exit(1)
    return user_token_dict

get_github_pat_from_gitcredentials(credentials_path)

Gets GitHub users and PAT from .gitconfig.

Parameters:

Name Type Description Default
credentials_path Path

Path to folder containing .git-credentials

required

Returns:

Type Description
dict[str, str]

dict[str, str]: A dict with user as key and PAT as value.

Source code in ssb_project_cli/ssb_project/create/github.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
def get_github_pat_from_gitcredentials(credentials_path: Path) -> dict[str, str]:
    """Gets GitHub users and PAT from .gitconfig.

    Args:
        credentials_path: Path to folder containing .git-credentials

    Returns:
        dict[str, str]: A dict with user as key and PAT as value.
    """
    git_credentials_file = credentials_path.joinpath(Path(".git-credentials"))
    user_token_dict: dict[str, str] = {}

    if not git_credentials_file.exists():
        return user_token_dict

    with open(git_credentials_file) as f:
        lines = f.readlines()
        for line in lines:
            p = re.compile("https://([A-Za-z0-9_-]+):([A-Za-z0-9_]+)@github.com")
            res = p.match(line)

            if res:
                user = res.group(1)
                token = res.group(2)
                user_token_dict[user] = token

    return user_token_dict

get_github_pat_from_netrc(netrc_path)

Gets GitHub users and PAT from .netrc.

Parameters:

Name Type Description Default
netrc_path Path

Path to folder containing .netrc

required

Returns:

Type Description
dict[str, str]

dict[str, str]: A dict with user as key and PAT as value.

Source code in ssb_project_cli/ssb_project/create/github.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
def get_github_pat_from_netrc(netrc_path: Path) -> dict[str, str]:
    """Gets GitHub users and PAT from .netrc.

    Args:
        netrc_path: Path to folder containing .netrc

    Returns:
        dict[str, str]: A dict with user as key and PAT as value.
    """
    credentials_netrc_file = netrc_path.joinpath(Path(".netrc"))
    user_token_dict: dict[str, str] = {}

    if not credentials_netrc_file.exists():
        return user_token_dict

    with open(credentials_netrc_file) as f:
        lines = f.readlines()
        for line in lines:
            p = re.compile(
                "machine github.com login ([A-Za-z0-9_-]+) password ([A-Za-z0-9_]+)"
            )
            res = p.match(line)

            if res:
                user = res.group(1)
                token = res.group(2)
                user_token_dict[user] = token

    return user_token_dict

get_github_username(github, github_token)

Get the user's GitHub username.

If running on-prem, prompt the user to select their username from a list of organization members. Otherwise, retrieve the user's username from GitHub.

Parameters:

Name Type Description Default
github Github

An instance of the Github class.

required
github_token str

GitHub API token.

required

Returns:

Name Type Description
str str

The user's GitHub username.

Source code in ssb_project_cli/ssb_project/create/github.py
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
def get_github_username(github: Github, github_token: str) -> str:
    """Get the user's GitHub username.

    If running on-prem, prompt the user to select their username from a list of
    organization members. Otherwise, retrieve the user's username from GitHub.

    Args:
        github: An instance of the `Github` class.
        github_token: GitHub API token.

    Returns:
        str: The user's GitHub username.
    """
    if running_onprem(JUPYTER_IMAGE_SPEC):
        org_members = get_org_members(github_token)
        user_value: str = questionary.autocomplete(
            message="Enter your GitHub username:",
            choices=org_members,
            style=prompt_autocomplete_style,
            validate=lambda text: text.lower()
            in [member.lower() for member in org_members],
        ).ask()
        return user_value
    else:
        return github.get_user().login

get_org_members(github_token)

Returns a list of login names for all members of a GitHub organization.

Parameters:

Name Type Description Default
github_token str

GitHub API token.

required

Returns:

Name Type Description
list list[str]

A list of strings, where each string is the login name of a member of the organization.

Source code in ssb_project_cli/ssb_project/create/github.py
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
def get_org_members(github_token: str) -> list[str]:
    """Returns a list of login names for all members of a GitHub organization.

    Args:
        github_token: GitHub API token.

    Returns:
        list: A list of strings, where each string is the login name of a member of the organization.
    """
    # Set up the API endpoint URL and initial query parameters
    url = f"https://api.github.com/orgs/{GITHUB_ORG_NAME}/members"
    params = {"per_page": 100, "page": 1}
    headers = {"Authorization": f"Bearer {github_token}"}

    # Store usernames
    github_usernames = []

    while True:
        response = requests.get(
            url,
            headers=headers,
            params=params,
            timeout=20,
            verify="/etc/ssl/certs/ca-certificates.crt",
        )

        if response.status_code == 200:
            members = response.json()
            if len(members) == 0:
                break

            for member in members:
                github_usernames.append(member["login"])
            params["page"] += 1
        else:
            print("Error: could not retrieve member list")
            response_json, response_status = response.json(), response.status_code
            create_error_log(f"{response_json=}, {response_status=}", "get_org_members")
            exit(1)

    return github_usernames

is_github_repo(token, repo_name, github_org_name)

Checks if a Repository already exists in the organization.

Parameters:

Name Type Description Default
repo_name str

Repository name

required
token str

GitHub personal access token

required
github_org_name str

Name of GitHub organization

required

Returns:

Type Description
bool

True if the repository exists, else false.

Source code in ssb_project_cli/ssb_project/create/github.py
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def is_github_repo(token: str, repo_name: str, github_org_name: str) -> bool:
    """Checks if a Repository already exists in the organization.

    Args:
        repo_name:  Repository name
        token: GitHub personal access token
        github_org_name: Name of GitHub organization

    Returns:
        True if the repository exists, else false.
    """
    try:
        get_environment_specific_github_object(token).get_repo(
            f"{github_org_name}/{repo_name}"
        )
    except ValueError:
        print(
            "The provided Github credentials are invalid. Please check that your personal access token is not expired."
        )
        exit(1)
    except GithubException:
        return False
    else:
        return True

set_branch_protection_rules(github_token, repo_name, github_org_name)

Sets branch default protection rules.

The following rules are set: Main branch pull requests requires a minimum of 1 reviewer. Reviews that are no longer valid can be dismissed. When you dismiss a review, you must add a comment explaining why you dismissed it.

Parameters:

Name Type Description Default
github_token str

GitHub personal access token

required
repo_name str

name of repository

required
github_org_name str

Name of GitHub organization

required
Source code in ssb_project_cli/ssb_project/create/github.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
def set_branch_protection_rules(
    github_token: str, repo_name: str, github_org_name: str
) -> None:
    """Sets branch default protection rules.

    The following rules are set:
    Main branch pull requests requires a minimum of 1 reviewer.
    Reviews that are no longer valid can be dismissed.
    When you dismiss a review, you must add a comment explaining why you dismissed it.

    Args:
        github_token: GitHub personal access token
        repo_name: name of repository
        github_org_name: Name of GitHub organization
    """
    repo = get_environment_specific_github_object(github_token).get_repo(
        f"{github_org_name}/{repo_name}"
    )
    repo.get_branch("main").edit_protection(
        required_approving_review_count=1,
        dismiss_stale_reviews=True,
        enforce_admins=True,
        # Need to supply the line under as a workaround until PyGithub is updated to v2
        users_bypass_pull_request_allowances=[],
    )

valid_repo_name(name)

Checks if the supplied name is suitable for a git repo.

Accepts
  • ASCII characters upper and lower case
  • Underscores
  • Hyphens
  • 3 characters or longer

Parameters:

Name Type Description Default
name str

Supplied repo name

required

Returns:

Name Type Description
bool bool

True if the string is a valid repo name

Source code in ssb_project_cli/ssb_project/create/github.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
def valid_repo_name(name: str) -> bool:
    """Checks if the supplied name is suitable for a git repo.

    Accepts:
     - ASCII characters upper and lower case
     - Underscores
     - Hyphens
     - 3 characters or longer

    Args:
        name: Supplied repo name

    Returns:
        bool: True if the string is a valid repo name
    """
    return len(name) >= 3 and re.fullmatch("^[a-zA-Z0-9-_]+$", name) is not None