Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion github_rest_api/github.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,45 @@
"""Simple wrapper of GitHub REST APIs."""

import re
from abc import ABCMeta, abstractmethod
from base64 import b64encode
from collections.abc import Sequence
from enum import StrEnum
from typing import Any, Callable
from pathlib import Path
from typing import Any, Callable

import requests
from nacl import encoding, public

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

_SECRET_NAME_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")


def _validate_secret_name(name: str) -> None:
"""Validate a secret name against GitHub's naming rules.

GitHub rejects invalid secret names with a 422 response. Validating the
name client-side surfaces a clear error before the request is sent.

:param name: The name of the secret.
:raises ValueError: If the name is empty, starts with the reserved
``GITHUB_`` prefix, starts with a digit, or contains characters other
than alphanumerics and underscores.
"""
if not name:
raise ValueError("A secret name must not be empty.")
if name.upper().startswith("GITHUB_"):
raise ValueError(
f"Invalid secret name {name!r}: names must not start with the "
"reserved 'GITHUB_' prefix."
)
if not _SECRET_NAME_PATTERN.fullmatch(name):
raise ValueError(
f"Invalid secret name {name!r}: names may only contain alphanumeric "
"characters and underscores, and must not start with a digit."
)
Comment thread
dclong marked this conversation as resolved.


def _encrypt_secret(public_key: str, value: str) -> str:
"""Encrypt a secret value using a LibSodium sealed box.
Expand Down Expand Up @@ -354,6 +383,7 @@ def create_or_update_secret(
automatically. Fetch it once and reuse it to avoid a redundant
request when creating or updating multiple secrets.
"""
_validate_secret_name(name)
if public_key is None:
public_key = self.get_secret_public_key()
return self._put(
Expand Down Expand Up @@ -549,6 +579,7 @@ def create_or_update_secret(
:param selected_repository_ids: Repository IDs that can access the secret
when visibility is `selected`.
"""
_validate_secret_name(name)
if selected_repository_ids and visibility != SecretVisibility.SELECTED:
raise ValueError(
"`selected_repository_ids` can only be provided when `visibility` is 'selected'."
Expand Down
14 changes: 8 additions & 6 deletions github_rest_api/scripts/cargo/benchmark.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
"""Benchmark action using cargo criterion."""

from typing import Callable
import tempfile
from pathlib import Path
import datetime
import shutil
import subprocess as sp
import tempfile
from pathlib import Path
from typing import Callable

from dulwich import porcelain

from ..utils import (
commit_benchmarks,
config_git,
switch_branch,
push_branch,
gen_temp_branch,
commit_benchmarks,
push_branch,
switch_branch,
)


Expand Down
12 changes: 7 additions & 5 deletions github_rest_api/scripts/cargo/profiling.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
"""Utils for profiling Rust applications."""

from typing import Iterable
from pathlib import Path
import time
import datetime
import subprocess as sp
import time
from pathlib import Path
from typing import Iterable

import psutil
from .utils import build_project
from ..utils import config_git, switch_branch, push_branch, commit_profiling

from ...utils import partition
from ..utils import commit_profiling, config_git, push_branch, switch_branch
from .utils import build_project


def launch_application(cmd: list[str]) -> int:
Expand Down
13 changes: 7 additions & 6 deletions github_rest_api/scripts/container/build_container_images.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import argparse
from collections.abc import Sequence
import datetime
from pathlib import Path
import subprocess as sp
import sys
from collections.abc import Sequence
from pathlib import Path
from typing import cast

import yaml
from dulwich.repo import Repo
from dulwich.refs import Ref
from dulwich.objects import Commit
from dulwich.errors import NotGitRepository
from dulwich.diff_tree import tree_changes
from dulwich.errors import NotGitRepository
from dulwich.objects import Commit
from dulwich.refs import Ref
from dulwich.repo import Repo
from tenacity import retry, stop_after_attempt, wait_exponential


Expand Down
5 changes: 3 additions & 2 deletions github_rest_api/scripts/container/config_container.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import argparse
import json
import shutil
import subprocess as sp
import sys
import tomllib
import tomli_w
from pathlib import Path
import subprocess as sp

import tomli_w


def config_docker(data_root: str = "/mnt/docker"):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import argparse
import datetime
import os
import re
import sys
from pathlib import Path
import re

from dulwich import porcelain
from requests.exceptions import HTTPError

from github_rest_api import Repository
from github_rest_api.utils import next_minor_or_strip_patch
from requests.exceptions import HTTPError


def parse_latest_version(repo: str) -> str:
Expand Down
3 changes: 2 additions & 1 deletion github_rest_api/scripts/github/add_github_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
import sys
from collections.abc import Sequence
from pathlib import Path

from dulwich import porcelain

from github_rest_api import User, Organization
from github_rest_api import Organization, User


def _validate_repo(repo: str) -> None:
Expand Down
3 changes: 2 additions & 1 deletion github_rest_api/scripts/github/create_pull_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
The branch is updated (using dev) before creating the PR.
"""

from argparse import ArgumentParser, Namespace
import os
import sys
from argparse import ArgumentParser, Namespace

from github_rest_api import Repository
from github_rest_api.utils import compile_patterns

Expand Down
5 changes: 3 additions & 2 deletions github_rest_api/scripts/github/release_on_github.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import argparse
import getpass
import os
import re
import sys
import argparse
import getpass
from pathlib import Path

from github_rest_api import Repository
from github_rest_api.scripts.utils import (
find_project_root,
Expand Down
3 changes: 2 additions & 1 deletion github_rest_api/scripts/github/remove_branch.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import argparse
import datetime
import re
import sys
import datetime

from github_rest_api import Repository


Expand Down
7 changes: 4 additions & 3 deletions github_rest_api/scripts/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Util functions for GitHub actions."""

import random
import tomllib
from typing import Any, Iterable
from pathlib import Path
from collections.abc import Sequence
import random
from pathlib import Path
from typing import Any, Iterable

from dulwich import porcelain
from dulwich.repo import Repo

Expand Down
4 changes: 2 additions & 2 deletions github_rest_api/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Some generally useful util functions."""

from collections.abc import Sequence
from itertools import tee, filterfalse
import re
from collections.abc import Sequence
from itertools import filterfalse, tee


def partition(pred, iterable):
Expand Down
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ requires = [ "hatchling" ]

[project]
name = "github-rest-api"
version = "0.42.0"
version = "0.42.1"
description = "Simple wrapper of GitHub REST APIs."
readme = "README.md"
authors = [ { name = "Ben Du", email = "longendu@yahoo.com" } ]
Expand Down Expand Up @@ -41,3 +41,8 @@ dev = [
"ruff>=0.14.10",
"ty>=0.0.8",
]

[tool.ruff.lint]
# select the 'I' rule set for import sorting
extend-select = ["I"]

1 change: 1 addition & 0 deletions tests/test_build_container_images.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from pathlib import Path
from unittest.mock import patch

from github_rest_api.scripts.container.build_container_images import (
has_relevant_changes,
)
Expand Down
37 changes: 36 additions & 1 deletion tests/test_github.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import os
from base64 import b64decode

import pytest
from nacl import encoding, public
from github_rest_api.github import User, Organization, Repository, _encrypt_secret

from github_rest_api.github import (
Organization,
Repository,
User,
_encrypt_secret,
_validate_secret_name,
)
Comment thread
dclong marked this conversation as resolved.

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

Expand All @@ -14,6 +23,32 @@ def test_encrypt_secret_roundtrip():
assert decrypted == b"s3cret-value"


@pytest.mark.parametrize(
"name",
["MY_SECRET", "_underscore", "Token123", "a"],
)
def test_validate_secret_name_valid(name):
_validate_secret_name(name)


@pytest.mark.parametrize(
"name",
[
"",
"GITHUB_ACTIONS",
"GITHUB_TOKEN",
"github_token",
"GitHub_Token",
"1SECRET",
"MY-SECRET",
"MY SECRET",
],
)
def test_validate_secret_name_invalid(name):
with pytest.raises(ValueError):
_validate_secret_name(name)
Comment thread
dclong marked this conversation as resolved.


def test_user_get_repositories():
user = User(TOKEN, "dclong")
repos = user.get_repositories()
Expand Down
2 changes: 2 additions & 0 deletions tests/test_release_on_github.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from pathlib import Path
from unittest.mock import patch

import pytest

from github_rest_api.scripts.github.release_on_github import _get_release_tag

ROOT = Path(".")
Expand Down
1 change: 1 addition & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest

from github_rest_api.utils import next_minor_or_strip_patch, strip_patch_version


Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.