Created
February 5, 2026 08:51
-
-
Save ekohl/f7ef0c372f9c151fd6b90c7c3044f69c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| import sys | |
| from pathlib import Path | |
| from subprocess import check_output | |
| import requests | |
| import yaml | |
| from github import Github | |
| from github.GithubException import UnknownObjectException | |
| # TODO: configurable or autodetect? | |
| HAS_OTP = False | |
| RELEASE_FLOW_NAME = 'release' | |
| ENVIRONMENT = 'release' | |
| GITHUB_SECRET = 'github/changelog' | |
| def get_secret(name: str) -> str: | |
| return check_output(['pass', 'show', name], universal_newlines=True).splitlines()[0] | |
| class Rubygems: | |
| BASE_URL = 'https://rubygems.org/api/v1' | |
| def __init__(self, api_key: str, otp: str | None = None): | |
| self._session = requests.Session() | |
| self._session.headers['Authorization'] = api_key | |
| if otp: | |
| self._session.headers['OTP'] = otp | |
| def publishers(self, gem: str) -> list: | |
| url = f'{self.BASE_URL}/gems/{gem}/trusted_publishers' | |
| response = self._session.get(url) | |
| response.raise_for_status() | |
| # TODO: Somehow this never lists any publishers and always returns an empty list | |
| return response.json() | |
| def create_publisher(self, gem: str, owner: str, repository: str, filename: str, | |
| environment: str): | |
| url = f'{self.BASE_URL}/gems/{gem}/trusted_publishers' | |
| data = { | |
| "trusted_publisher_type": "OIDC::TrustedPublisher::GitHubAction", | |
| "trusted_publisher": { | |
| "repository_owner": owner, | |
| "repository_name": repository or gem, | |
| "workflow_filename": filename, | |
| "environment": environment, | |
| } | |
| } | |
| response = self._session.post(url, json=data) | |
| response.raise_for_status() | |
| return response.json() | |
| def read_rubygems_api_key() -> str | None: | |
| # TODO: ~/.config | |
| for path in ('~/.gem/credentials',): | |
| expanded = Path(path).expanduser() | |
| if expanded.exists(): | |
| data = yaml.safe_load(expanded.read_bytes()) | |
| if data and ':rubygems_api_key' in data: | |
| return data[':rubygems_api_key'] | |
| else: | |
| return None | |
| def main(): | |
| repo_name = sys.argv[1] | |
| token = get_secret(GITHUB_SECRET) | |
| github = Github(token) | |
| repo = github.get_repo(repo_name) | |
| for content in repo.get_contents('/'): | |
| if content.path.endswith('.gemspec'): | |
| gem_name = content.path.removesuffix('.gemspec') | |
| break | |
| else: | |
| raise SystemExit(f'Could not find a gemspec for {repo_name}') | |
| for content in repo.get_contents('/.github/workflows'): | |
| if content.name.startswith(RELEASE_FLOW_NAME): | |
| filename = content.name | |
| break | |
| else: | |
| raise SystemExit(f'Could not find a release workflow for {repo_name}') | |
| try: | |
| repo.get_environment(ENVIRONMENT) | |
| except UnknownObjectException: | |
| repo.create_environment(ENVIRONMENT) | |
| api_key = read_rubygems_api_key() | |
| if not api_key: | |
| raise SystemExit('Could not read API key') | |
| otp = input('OTP: ') if HAS_OTP else None | |
| rubygems = Rubygems(api_key, otp) | |
| # TODO: check if a valid publisher already exists | |
| try: | |
| publisher = rubygems.create_publisher(gem=gem_name, owner=repo.owner.login, | |
| repository=repo.name, filename=filename, | |
| environment=ENVIRONMENT) | |
| except requests.RequestException as e: | |
| print('Unable to create trusted publisher', file=sys.stderr) | |
| print(e, file=sys.stderr) | |
| if e.response: | |
| print(e.reponse.json(), file=sys.stderr) | |
| else: | |
| print('Created trusted publisher', publisher['trusted_publisher']['name']) | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment