OpenMiInk/third_party/ci-notary-service/sftpnotary/buildpublisher.py
Hellings b8c8ff98e2 Hi
2026-10-16 09:05:56 -04:00

163 lines
5.7 KiB
Python

# SPDX-License-Identifier: BSD-2-Clause
# SPDX-FileCopyrightText: 2023 KDE e.V.
# SPDX-FileCopyrightText: 2023 Ben Cooksley <bcooksley@kde.org>
# SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
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