|
1 | 1 | """Simple wrapper of GitHub REST APIs.""" |
2 | 2 |
|
| 3 | +import re |
3 | 4 | from abc import ABCMeta, abstractmethod |
4 | 5 | from base64 import b64encode |
5 | 6 | from collections.abc import Sequence |
6 | 7 | from enum import StrEnum |
7 | | -from typing import Any, Callable |
8 | 8 | from pathlib import Path |
| 9 | +from typing import Any, Callable |
| 10 | + |
9 | 11 | import requests |
10 | 12 | from nacl import encoding, public |
11 | 13 |
|
12 | 14 | URL_API = "https://api.github.com" |
13 | 15 |
|
| 16 | +_SECRET_NAME_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") |
| 17 | + |
| 18 | + |
| 19 | +def _validate_secret_name(name: str) -> None: |
| 20 | + """Validate a secret name against GitHub's naming rules. |
| 21 | +
|
| 22 | + GitHub rejects invalid secret names with a 422 response. Validating the |
| 23 | + name client-side surfaces a clear error before the request is sent. |
| 24 | +
|
| 25 | + :param name: The name of the secret. |
| 26 | + :raises ValueError: If the name is empty, starts with the reserved |
| 27 | + ``GITHUB_`` prefix, starts with a digit, or contains characters other |
| 28 | + than alphanumerics and underscores. |
| 29 | + """ |
| 30 | + if not name: |
| 31 | + raise ValueError("A secret name must not be empty.") |
| 32 | + if name.upper().startswith("GITHUB_"): |
| 33 | + raise ValueError( |
| 34 | + f"Invalid secret name {name!r}: names must not start with the " |
| 35 | + "reserved 'GITHUB_' prefix." |
| 36 | + ) |
| 37 | + if not _SECRET_NAME_PATTERN.fullmatch(name): |
| 38 | + raise ValueError( |
| 39 | + f"Invalid secret name {name!r}: names may only contain alphanumeric " |
| 40 | + "characters and underscores, and must not start with a digit." |
| 41 | + ) |
| 42 | + |
14 | 43 |
|
15 | 44 | def _encrypt_secret(public_key: str, value: str) -> str: |
16 | 45 | """Encrypt a secret value using a LibSodium sealed box. |
@@ -354,6 +383,7 @@ def create_or_update_secret( |
354 | 383 | automatically. Fetch it once and reuse it to avoid a redundant |
355 | 384 | request when creating or updating multiple secrets. |
356 | 385 | """ |
| 386 | + _validate_secret_name(name) |
357 | 387 | if public_key is None: |
358 | 388 | public_key = self.get_secret_public_key() |
359 | 389 | return self._put( |
@@ -549,6 +579,7 @@ def create_or_update_secret( |
549 | 579 | :param selected_repository_ids: Repository IDs that can access the secret |
550 | 580 | when visibility is `selected`. |
551 | 581 | """ |
| 582 | + _validate_secret_name(name) |
552 | 583 | if selected_repository_ids and visibility != SecretVisibility.SELECTED: |
553 | 584 | raise ValueError( |
554 | 585 | "`selected_repository_ids` can only be provided when `visibility` is 'selected'." |
|
0 commit comments