# SPDX-License-Identifier: BSD-2-Clause # SPDX-FileCopyrightText: 2023 KDE e.V. # SPDX-FileCopyrightText: 2023 Ben Cooksley # SPDX-FileContributor: Ingo Klöcker import dataclasses import logging import os import tarfile from pathlib import Path from tempfile import TemporaryDirectory from typing import Optional from sftpnotary import projects, util from sftpnotary.core import Job, Request, Response, TaskProcessor, Worker from sftpnotary.exceptions import Error, InvalidParameter from sftpnotary.sftp import SFTPClient, SFTPError log = logging.getLogger(__name__) @dataclasses.dataclass class PublishBuildRequest(Request): buildArchiveFileName: Optional[str] = None platform: Optional[str] = None def validate(self): # Ensure we don't have an evil build artifact name... util.validateFileName(self.buildArchiveFileName, strict=True) # Ensure the platform name is valid validPlatforms = [ "windows", "linux", "macos-x86_64", "macos-arm64", "macos-universal", "android-x86_64", "android-armeabi-v7a", "android-arm64-v8a", "snap-image", # Krita specific "windows-qt5", "linux-qt5", "macos-universal-qt5", # Temporary Kdenlive specific "msvc-experimental", ] if self.platform not in validPlatforms: raise InvalidParameter(name="buildPlatform", value=self.platform) PublishBuildResponse = Response class PublishBuildJob(Job): def __init__( self, sftp: SFTPClient, buildArchiveFilePath: Path, token: str, projectPath: str, branch: str, platform: str, ): super().__init__(sftp) self.buildArchiveFilePath = buildArchiveFilePath self.request = PublishBuildRequest( buildArchiveFileName=None, token=token, projectPath=projectPath, branch=branch, platform=platform, ) self.resultPath = self.buildArchiveFilePath.parent def start(self): buildArchiveFileName = self.buildArchiveFilePath.name self.sftp.upload(self.buildArchiveFilePath, self.task.path() / buildArchiveFileName) self.request.buildArchiveFileName = buildArchiveFileName self.task.putRequest(self.request) self.taskQueue.commitTask(self.task) def waitForCompletion(self): self.taskQueue.waitForCompletion(self.task) log.debug(f"Fetching response of task '{self.task.id}' ...") response = None try: response = self.task.fetchResponse(PublishBuildResponse) except SFTPError as e: log.error("Error: %s", e) if response is None: log.error("Error: Got no response. Task failed.") else: self.success = True self.downloadLogFiles(self.resultPath) self.taskQueue.removeTask(self.task) class PublishBuildProcessor(TaskProcessor): requestClass = PublishBuildRequest def getProjectSetting(self, name, required=True): value = projects.settings.get(self.request.projectPath, self.request.branch, name) if required and not value: raise Error( f"Project setting '{name}' not set for branch {self.request.branch} " f"of project {self.request.projectPath}" ) return value def doProcess(self): # Get the details for where we will publish the build artifacts to rsyncDestination = self.getProjectSetting("rsyncDestination") # Retrieve the project, branch and platform projectPath = self.request.projectPath branch = self.request.branch platform = self.request.platform # Convert any slashes in the branch name to dashes to keep folder names tidy branch = branch.replace("/", "-") with TemporaryDirectory() as workPathName: workPath = Path(workPathName) localBuildArchiveFilePath = workPath / self.request.buildArchiveFileName self.sftp.download(self.task.path() / self.request.buildArchiveFileName, localBuildArchiveFilePath) # Setup a path for us to unpack the build artifacts archive into... unpackedBuildArtifactsPath = workPath / "unpacked" os.makedirs(unpackedBuildArtifactsPath) # Unpack the build archive.... archive = tarfile.open(name=localBuildArchiveFilePath, mode="r") # Set "nosec B202" because of https://github.com/PyCQA/bandit/issues/1038#issuecomment-1635863856 archive.extractall(filter="tar", path=unpackedBuildArtifactsPath) # nosec B202 archive.close() # Determine the final rsync destination # We allow for the project path, branch and platform to be inserted via Python named string format variables # This avoids having to repeat ourselves in the configuration file finalRsyncDestination = rsyncDestination.format(projectPath=projectPath, branch=branch, platform=platform) # Build up the rsync command to run then run it # The entire contents of rsyncCommand = [ "rsync", "--archive", "--checksum", "--delete", "--mkpath", "--verbose", str(unpackedBuildArtifactsPath) + "/", finalRsyncDestination.rstrip("/") + "/", ] util.runCommand(rsyncCommand, cwd=workPathName) class PublishBuildWorker(Worker): processorClass = PublishBuildProcessor