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
12 changes: 11 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
name: Release
name: Main

on:
push:

jobs:
lint:
runs-on: ubuntu-slim
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
Expand All @@ -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:
Expand All @@ -35,6 +39,8 @@ jobs:

#test_freebsd:
# runs-on: ubuntu-latest
# permissions:
# contents: read
# steps:
# - uses: actions/checkout@v6
# - uses: vmactions/freebsd-vm@v1
Expand All @@ -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:
Expand Down
23 changes: 19 additions & 4 deletions bork/creds.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)
Expand All @@ -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:
Expand Down
7 changes: 0 additions & 7 deletions bork/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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):
Expand Down
34 changes: 16 additions & 18 deletions bork/pypi.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 5 additions & 9 deletions bork/trusted_publishing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}

Expand All @@ -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
Expand Down Expand Up @@ -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"]

Expand All @@ -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.")
Expand Down
2 changes: 1 addition & 1 deletion bork/version.py
Original file line number Diff line number Diff line change
@@ -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'