Skip to content

Commit 89d8b4b

Browse files
committed
feat: validate secret names client-side before API request
Add client-side validation for GitHub secret names in repository and organization secret creation methods to catch invalid names (e.g. starting with digits or "GITHUB_") before triggering API errors.
1 parent fa289a0 commit 89d8b4b

3 files changed

Lines changed: 65 additions & 2 deletions

File tree

github_rest_api/github.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,39 @@
66
from enum import StrEnum
77
from typing import Any, Callable
88
from pathlib import Path
9+
import re
910
import requests
1011
from nacl import encoding, public
1112

1213
URL_API = "https://api.github.com"
1314

15+
_SECRET_NAME_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
16+
17+
18+
def _validate_secret_name(name: str) -> None:
19+
"""Validate a secret name against GitHub's naming rules.
20+
21+
GitHub rejects invalid secret names with a 422 response. Validating the
22+
name client-side surfaces a clear error before the request is sent.
23+
24+
:param name: The name of the secret.
25+
:raises ValueError: If the name is empty, starts with the reserved
26+
``GITHUB_`` prefix, starts with a digit, or contains characters other
27+
than alphanumerics and underscores.
28+
"""
29+
if not name:
30+
raise ValueError("A secret name must not be empty.")
31+
if name.upper().startswith("GITHUB_"):
32+
raise ValueError(
33+
f"Invalid secret name {name!r}: names must not start with the "
34+
"reserved 'GITHUB_' prefix."
35+
)
36+
if not _SECRET_NAME_PATTERN.fullmatch(name):
37+
raise ValueError(
38+
f"Invalid secret name {name!r}: names may only contain alphanumeric "
39+
"characters and underscores, and must not start with a digit."
40+
)
41+
1442

1543
def _encrypt_secret(public_key: str, value: str) -> str:
1644
"""Encrypt a secret value using a LibSodium sealed box.
@@ -354,6 +382,7 @@ def create_or_update_secret(
354382
automatically. Fetch it once and reuse it to avoid a redundant
355383
request when creating or updating multiple secrets.
356384
"""
385+
_validate_secret_name(name)
357386
if public_key is None:
358387
public_key = self.get_secret_public_key()
359388
return self._put(
@@ -549,6 +578,7 @@ def create_or_update_secret(
549578
:param selected_repository_ids: Repository IDs that can access the secret
550579
when visibility is `selected`.
551580
"""
581+
_validate_secret_name(name)
552582
if selected_repository_ids and visibility != SecretVisibility.SELECTED:
553583
raise ValueError(
554584
"`selected_repository_ids` can only be provided when `visibility` is 'selected'."

tests/test_github.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import os
22
from base64 import b64decode
3+
import pytest
34
from nacl import encoding, public
4-
from github_rest_api.github import User, Organization, Repository, _encrypt_secret
5+
from github_rest_api.github import (
6+
User,
7+
Organization,
8+
Repository,
9+
_encrypt_secret,
10+
_validate_secret_name,
11+
)
512

613
TOKEN = os.environ.get("GITHUB_TOKEN", "")
714

@@ -14,6 +21,32 @@ def test_encrypt_secret_roundtrip():
1421
assert decrypted == b"s3cret-value"
1522

1623

24+
@pytest.mark.parametrize(
25+
"name",
26+
["MY_SECRET", "_underscore", "Token123", "a"],
27+
)
28+
def test_validate_secret_name_valid(name):
29+
_validate_secret_name(name)
30+
31+
32+
@pytest.mark.parametrize(
33+
"name",
34+
[
35+
"",
36+
"GITHUB_ACTIONS",
37+
"GITHUB_TOKEN",
38+
"github_token",
39+
"GitHub_Token",
40+
"1SECRET",
41+
"MY-SECRET",
42+
"MY SECRET",
43+
],
44+
)
45+
def test_validate_secret_name_invalid(name):
46+
with pytest.raises(ValueError):
47+
_validate_secret_name(name)
48+
49+
1750
def test_user_get_repositories():
1851
user = User(TOKEN, "dclong")
1952
repos = user.get_repositories()

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)