diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8b40490..c7f9a1a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: Release +name: Main on: push: @@ -6,6 +6,8 @@ on: jobs: lint: runs-on: ubuntu-slim + permissions: + contents: read steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v5 @@ -22,6 +24,8 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.11", "3.12", "3.13", "3.14"] runs-on: ${{ matrix.os }} + permissions: + contents: read steps: - uses: actions/checkout@v6 with: @@ -35,6 +39,8 @@ jobs: #test_freebsd: # runs-on: ubuntu-latest + # permissions: + # contents: read # steps: # - uses: actions/checkout@v6 # - uses: vmactions/freebsd-vm@v1 @@ -51,12 +57,16 @@ jobs: ci_success: name: "CI Success" runs-on: ubuntu-slim + permissions: + contents: read needs: [lint, test] steps: - run: true changes: runs-on: ubuntu-latest + permissions: + contents: read outputs: version_changed: ${{ steps.project.VERSION_CHANGED == '1' }} steps: diff --git a/bork/creds.py b/bork/creds.py index 0d07e3a..058d9f3 100644 --- a/bork/creds.py +++ b/bork/creds.py @@ -1,5 +1,7 @@ from os import getenv from typing import Optional +from . import trusted_publishing +from .log import logger from pydantic.dataclasses import dataclass @@ -9,11 +11,11 @@ class Credentials: pypi: Optional['Credentials.PyPI'] = None @classmethod - def from_env(cls) -> 'Credentials': + def from_env(cls, pypi_repository=None) -> 'Credentials': # TODO: support specifying another env than os.environ? return cls( github = getenv("BORK_GITHUB_TOKEN"), - pypi = cls.PyPI.from_env(), + pypi = cls.PyPI.from_env(pypi_repository), ) @dataclass(frozen = True) @@ -22,14 +24,27 @@ class PyPI: password: str @classmethod - def from_env(cls) -> Optional['Credentials.PyPI']: + def from_trusted_publishing(cls, repository) -> Optional['Credentials.PyPI']: + if repository is None: + return None + + logger().debug("trying to get token via Trusted Publishing") + token = trusted_publishing.get_token(repository) + if token is not None: + return cls("__token__", token) + + return None + + # FIXME: Avoid needing to pass around repository, BUT ALSO respect --pypi-repository/--test-pypi + @classmethod + def from_env(cls, repository=None) -> Optional['Credentials.PyPI']: match getenv("BORK_PYPI_USERNAME"), getenv("BORK_PYPI_PASSWORD"), getenv("BORK_PYPI_TOKEN"): case username, password, None if username and password: return cls(username, password) case None, None, token if token: return cls("__token__", token) case None, None, None: - return None + return cls.from_trusted_publishing(repository) # Error cases case _, password, token if password and token: diff --git a/bork/http.py b/bork/http.py index 0310aa5..8cddf17 100644 --- a/bork/http.py +++ b/bork/http.py @@ -2,13 +2,10 @@ import urllib3 from . import version -from .log import logger MAX_RETRIES = False def request(method, url, fields, auth): - log = logger() - user_agent = f"bork/{version.__version__} (+https://github.com/duckinator/bork)" http = urllib3.PoolManager() @@ -18,10 +15,6 @@ def request(method, url, fields, auth): if 399 < response.status < 500: raise RuntimeError(response.data.decode()) - log.debug(response.getheaders()) - - log.debug("%s %s returned %i", method, response.geturl(), response.status) - return response def get(url, auth=None): diff --git a/bork/pypi.py b/bork/pypi.py index a935f69..f660f44 100644 --- a/bork/pypi.py +++ b/bork/pypi.py @@ -1,8 +1,8 @@ import hashlib -import os from pathlib import Path -from . import builder, trusted_publishing +from . import builder +from .creds import Credentials from .filesystem import find_files, wheel_file_info from .log import logger from .http import post @@ -37,14 +37,6 @@ def __init__(self, files, repository=None): self.files = files self.repository = repository - self.username = os.environ.get("BORK_PYPI_USERNAME", None) - self.password = os.environ.get("BORK_PYPI_PASSWORD", None) - - token = os.environ.get("BORK_PYPI_TOKEN", None) - if self.username is None and token is not None: - self.username = "__token__" - self.password = token - def _upload_file(self, url, file, metadata): file_contents = Path(file).read_bytes() file_digest = hashlib.sha256(file_contents).hexdigest() @@ -119,20 +111,15 @@ def _upload_file(self, url, file, metadata): *other_fields ] - # If we've still have no credentials, try Trusted Publishing. - if self.username is None and self.password is None: - token = trusted_publishing.get_token(self.repository) - if token is not None: - self.username = "__token__" - self.password = token + username, password = self._get_credentials() - if self.username is None and self.password is None: + if username is None and password is None: raise RuntimeError( "BORK_PYPI_USERNAME and BORK_PYPI_PASSWORD environment variables are undefined.\n\n" "If you used Bork prior to v9.0.0, these variables used to be TWINE_USERNAME and " "TWINE_PASSWORD. You can use the same values.") - response = post(url, form, auth=(self.username, self.password)) + response = post(url, form, auth=(username, password)) return response def upload(self, *, dry_run = True, metadata = None): @@ -162,6 +149,17 @@ def upload(self, *, dry_run = True, metadata = None): log.info("FAILED - %s couldn't be uploaded to %s", filename, self.repository) log.info(response.data.decode().strip()) + def _get_credentials(self): + username = None + password = None + + credentials = Credentials.from_env(self.repository) + if credentials.pypi: + username = credentials.pypi.username + password = credentials.pypi.password + + return (username, password) + def upload(repository_name, *globs, **kwargs): files = find_files(globs) diff --git a/bork/trusted_publishing.py b/bork/trusted_publishing.py index 0e61202..a8dfa73 100644 --- a/bork/trusted_publishing.py +++ b/bork/trusted_publishing.py @@ -2,15 +2,13 @@ from .log import logger import json import os -import urllib +import urllib.request from urllib.parse import urlsplit # FIXME: Dedupe request/get/post with bork/github_api.py. def request(method, url, data, headers): - log = logger() - if headers is None: headers = {} @@ -21,10 +19,8 @@ def request(method, url, data, headers): req = urllib.request.Request(url, data=data, headers=headers, method=method) - log.debug('%s %s', req.method, req.full_url) with urllib.request.urlopen(req) as res: - log.debug('-> %s returned %i %s', res.url, res.status, res.reason) response = res.read().decode() return response @@ -54,7 +50,7 @@ def detected(): def get_token(self, repository): """Perform the whole song and dance to get a token.""" - url = f"{repository}/_/oidc/mint-token" + url = urlsplit(repository)._replace(path="/_/oidc/mint-token").geturl() data = json.loads(post(url, {"token": self.get_ambient_credential()})) return data["token"] @@ -66,11 +62,11 @@ class GithubTrustedPublishing(TrustedPublishingProvider): @staticmethod def detected(): """Are we running on GitHub CI?""" - return bool(os.environ.get("GITHUB_CI")) + return bool(os.environ.get("CI") and os.environ.get("GITHUB_ACTION")) def get_ambient_credential(self): - token = os.environ("ACTIONS_ID_TOKEN_REQUEST_TOKEN") - url = os.environ("ACTIONS_ID_TOKEN_REQUEST_URL") + token = os.environ.get("ACTIONS_ID_TOKEN_REQUEST_TOKEN") + url = os.environ.get("ACTIONS_ID_TOKEN_REQUEST_URL") if not token: raise TrustedPublishingError("Expected ACTIONS_ID_TOKEN_REQUEST_TOKEN environment variable to be defined.") diff --git a/bork/version.py b/bork/version.py index 71a5d25..e97ac64 100644 --- a/bork/version.py +++ b/bork/version.py @@ -1,4 +1,4 @@ # This file should only ever be modified to change the version. # This will automatically prepare and create a release. -__version__ = '11.0.0b5' +__version__ = '11.0.0'