Gitea/Forgejo Package Uploader
Gitea and Forgejo are pretty rad Git servers which also ship with package registries for a bunch of different languages. In a language such as .NET uploading a package is as simple as registering the server/source:
dotnet nuget add source --name my-nuget --username user --password password https://gitea.example.com/api/packages/someuser/nuget/index.json
Followed by a publishing command:
dotnet nuget push --source my-nuget some-package.nupkg
Most of the Gitea/Forgejo's package registries can be accessed by a simple POST
call, so I started to fiddle around with a Python script called forgejo-push
which could give me the same kind of neat experience when dealing with other
registries such as Debian, Red Hat or the generic package registry.
So let's start with the ~/.config/forgejo-push.conf
configuration file:
[my-debian] url = https://gitea.example.com/api/packages/someuser/debian/pool/bookworm/main/upload token = some-token [my-rpm] url = https://gitea.example.com/api/packages/someuser/rpm/upload token = another-token [my-generic] url = https://gitea.example.com/api/packages/someuser/generic/ token = some-other-token
Follow it up with some examples:
# Upload all .deb files using a recursive glob pattern: forgejo-push --glob '**/*.deb' --section 'my-debian' # Upload a single .rpm files: forgejo-push --glob 'my-package.rpm' --section 'my-rpm' # Do not throw an error when a package already exists: forgejo-push --glob 'my-package.rpm' --section 'my-rpm' --skip-duplicate # The generic registry is different since it requires more parameters, so we # squeeze them in using a hacky --postfix parameter: forgejo-push --glob 'artifacts/my-binary1' --section 'my-generic' --postfix 'my-pkg/1.0.0/my-binary1' forgejo-push --glob 'artifacts/my-binary2' --section 'my-generic' --postfix 'my-pkg/1.0.0/my-binary2' # The --postfix flag also supports a {file_name} placeholder, so the above two # lines can be changed to: forgejo-push --glob 'artifacts/my-*' --section 'my-generic' --postfix 'my-pkg/1.0.0/{file_name}'
And finally, show the code in all its bare and unpolished glory:
#!/usr/bin/env python3 import argparse import collections import configparser import glob import os import requests class Config(collections.namedtuple('Config', 'url token')): __slots__ = () def parse_config(text): config_parser = configparser.ConfigParser() config_parser.read_string(text) config = {} for section in config_parser.sections(): section_data = config_parser[section] url = section_data.get('url') token = section_data.get('token') config[section] = Config(url, token) return config def load_config(profile): with open(profile, 'r') as f: return parse_config(f.read()) def upload_package(url, token, path_name, skip_duplicate): headers = { 'Authorization': f'token {token}' } with open(path_name, 'rb') as f: response = requests.put(url, headers=headers, data=f.read()) if response.ok or (response.status_code == 409 and skip_duplicate): print(f'{path_name}: {url}') else: raise SystemExit(f'Could not upload {path_name}: {response.reason} ({response.status_code})') def main(): parser = argparse.ArgumentParser(description='Upload packages to Gitea/Forgejo.') parser.add_argument('--glob', help='The file glob pattern to upload', required=True) parser.add_argument('--section', help='The config file section name', required=True) parser.add_argument('--postfix', help='An optional URL postfix', default='') parser.add_argument('--skip-duplicate', help='Succeed when uploading a duplicate package', action='store_true') args = parser.parse_args() path_names = glob.glob(args.glob, recursive=True) config = load_config(os.path.expanduser('~/.config/forgejo-push.conf'))[args.section] for path_name in path_names: url = config.url + args.postfix.replace('{file_name}', os.path.basename(path_name)) upload_package(url, config.token, path_name, args.skip_duplicate) if __name__ == '__main__': main()