#!/usr/bin/env python3 # SPDX-License-Identifier: BSD-2-Clause # # SPDX-FileCopyrightText: 2023 KDE e.V. # SPDX-FileCopyrightText: 2023 Ben Cooksley # SPDX-FileContributor: Ingo Klöcker import logging import os import sys import tarfile from pathlib import Path from tempfile import NamedTemporaryFile from sftpnotary import config, projects, util from sftpnotary.exceptions import Error from sftpnotary.pypipublisher import PublishPyPiJob log = logging.getLogger("publishonpypi") def parseCommandLine(): import argparse parser = argparse.ArgumentParser(description="Publish Python packages to a PyPi repository") # debug options parser.add_argument("-v", "--verbose", action="count", default=0, help="increase the verbosity") parser.add_argument("--config", required=True, help="the configuration to use; required") parser.add_argument( "--repository", required=True, help="the repository the package should be published to; required" ) parser.add_argument( "folderToPublish", help='the folder containing the package to be published (eg. the "dist" folder); required' ) options = parser.parse_args() return options def logTaskLog(logFile: Path): log.info("Logs of remote signing:") with open(logFile, "r", encoding="utf-8") as f: for line in f: log.info(line.rstrip()) def publishPyPi(archiveFilePath: Path, projectPath: str, branch: str, repository: str, logDestination: Path) -> bool: with util.connectToSftpServer() as sftp: job = PublishPyPiJob( sftp, archiveFilePath=archiveFilePath, token=os.environ.get("CI_JOB_TOKEN"), projectPath=projectPath, branch=branch, repository=repository, ) try: job.start() job.waitForCompletion(logDestination.parent) except Exception as e: # or more specific exception log.error("Error: PublishPyPiJob failed", exc_info=e) for logFile in job.logFiles: if logFile.name == "task.log": logTaskLog(logFile) if not job.success: log.error("Error: Publishing to PyPi failed") return job.success def main() -> int: options = parseCommandLine() config.loadConfig(options.config) util.setUpClientLogging(log, options.verbose) projectSettingsLocation = config.settings.get("General", "ProjectSettings", "pypipublisher-projects.yaml") util.loadProjectSettings( projectSettingsLocation, relativeTo=Path(options.config).absolute().parent, ) # Ignore merge request pipelines if os.environ.get("CI_PIPELINE_SOURCE") == "merge_request_event": log.info("Merge request pipelines are not cleared for publishing. Skipping.") return 0 # Check if it makes sense to attempt publishing to pypi projectPath = os.environ["CI_PROJECT_PATH"] branch = os.environ["CI_COMMIT_REF_NAME"] repository = options.repository if not projects.settings.exists(projectPath, branch): log.info( f"""Branch '{branch}' of project '{projectPath}' is not cleared for publishing. Skipping.\n You can clear it by adding the branch to {projectSettingsLocation}""" ) return 0 # Build a tarball of the wheel files and trigger the publishing process with NamedTemporaryFile() as wheelArchive: # Open our temporary file as a tarball archive = tarfile.open(fileobj=wheelArchive, mode="w") # Determine the path to be archived and the files to be included pathToArchive = Path(options.folderToPublish) filesToInclude = os.listdir(pathToArchive) # Start populating it with the wheel files for filename in filesToInclude: fullPath = pathToArchive / filename archive.add(fullPath, arcname=filename, recursive=True) # Close it out to ensure everything is flushed to disk archive.close() wheelArchive.flush() success = publishPyPi(Path(wheelArchive.name), projectPath, branch, repository, pathToArchive) return 0 if success else 1 if __name__ == "__main__": try: sys.exit(main()) except Error as e: print("Error: %s" % e, file=sys.stderr) sys.exit(1)