get:
Show a patch.

patch:
Update a patch.

put:
Update a patch.

GET /api/patches/135969/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 135969,
    "url": "https://patches.dpdk.org/api/patches/135969/?format=api",
    "web_url": "https://patches.dpdk.org/project/ci/patch/20240118234120.29256-2-ahassick@iol.unh.edu/",
    "project": {
        "id": 5,
        "url": "https://patches.dpdk.org/api/projects/5/?format=api",
        "name": "CI",
        "link_name": "ci",
        "list_id": "ci.dpdk.org",
        "list_email": "ci@dpdk.org",
        "web_url": "",
        "scm_url": "git://dpdk.org/tools/dpdk-ci",
        "webscm_url": "https://git.dpdk.org/tools/dpdk-ci/",
        "list_archive_url": "https://inbox.dpdk.org/ci",
        "list_archive_url_format": "https://inbox.dpdk.org/ci/{}",
        "commit_url_format": ""
    },
    "msgid": "<20240118234120.29256-2-ahassick@iol.unh.edu>",
    "list_archive_url": "https://inbox.dpdk.org/ci/20240118234120.29256-2-ahassick@iol.unh.edu",
    "date": "2024-01-18T23:41:18",
    "name": "[v3,1/3] tools: Add script to create artifacts",
    "commit_ref": null,
    "pull_url": null,
    "state": "new",
    "archived": false,
    "hash": "2bd64d3ff49fbafba31300c0c69a8a2c61a38bb4",
    "submitter": {
        "id": 3127,
        "url": "https://patches.dpdk.org/api/people/3127/?format=api",
        "name": "Adam Hassick",
        "email": "ahassick@iol.unh.edu"
    },
    "delegate": null,
    "mbox": "https://patches.dpdk.org/project/ci/patch/20240118234120.29256-2-ahassick@iol.unh.edu/mbox/",
    "series": [
        {
            "id": 30842,
            "url": "https://patches.dpdk.org/api/series/30842/?format=api",
            "web_url": "https://patches.dpdk.org/project/ci/list/?series=30842",
            "date": "2024-01-18T23:41:17",
            "name": "Add a script to create series artifacts",
            "version": 3,
            "mbox": "https://patches.dpdk.org/series/30842/mbox/"
        }
    ],
    "comments": "https://patches.dpdk.org/api/patches/135969/comments/",
    "check": "pending",
    "checks": "https://patches.dpdk.org/api/patches/135969/checks/",
    "tags": {},
    "related": [],
    "headers": {
        "Return-Path": "<ci-bounces@dpdk.org>",
        "X-Original-To": "patchwork@inbox.dpdk.org",
        "Delivered-To": "patchwork@inbox.dpdk.org",
        "Received": [
            "from mails.dpdk.org (mails.dpdk.org [217.70.189.124])\n\tby inbox.dpdk.org (Postfix) with ESMTP id 59106438F8;\n\tFri, 19 Jan 2024 00:42:11 +0100 (CET)",
            "from mails.dpdk.org (localhost [127.0.0.1])\n\tby mails.dpdk.org (Postfix) with ESMTP id 537C440EDF;\n\tFri, 19 Jan 2024 00:42:11 +0100 (CET)",
            "from mail-yw1-f179.google.com (mail-yw1-f179.google.com\n [209.85.128.179])\n by mails.dpdk.org (Postfix) with ESMTP id 4E83C40EDF\n for <ci@dpdk.org>; Fri, 19 Jan 2024 00:42:10 +0100 (CET)",
            "by mail-yw1-f179.google.com with SMTP id\n 00721157ae682-5ff7a8b5e61so1596517b3.2\n for <ci@dpdk.org>; Thu, 18 Jan 2024 15:42:10 -0800 (PST)",
            "from pogmachine2.loudonlune.net ([216.212.51.182])\n by smtp.gmail.com with ESMTPSA id\n oq6-20020a05620a610600b0078334ada139sm5625641qkn.7.2024.01.18.15.42.08\n (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n Thu, 18 Jan 2024 15:42:08 -0800 (PST)"
        ],
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=iol.unh.edu; s=unh-iol; t=1705621329; x=1706226129; darn=dpdk.org;\n h=content-transfer-encoding:mime-version:references:in-reply-to\n :message-id:date:subject:cc:to:from:from:to:cc:subject:date\n :message-id:reply-to;\n bh=nQENkoQAuKCQ6Wr3eWfryzCLhRU/VO+WItkY2Y5+yMM=;\n b=VHU7yipNnJIb98WJZaqPQQwsbIp7HIgWQdrzF8odaOnUVli/5SKdgUPVfB/nUFxhSr\n H75tEW6Z7fuQvJvrMzxXFrVHxLE31VwM4kWlIqPMAoAH5cWYivAbVWBEYvEzBeAEYrY9\n UfYCToaZ32N/wYDXGq0Uf9DgNd1wNge5/TU1s=",
        "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=1e100.net; s=20230601; t=1705621329; x=1706226129;\n h=content-transfer-encoding:mime-version:references:in-reply-to\n :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc\n :subject:date:message-id:reply-to;\n bh=nQENkoQAuKCQ6Wr3eWfryzCLhRU/VO+WItkY2Y5+yMM=;\n b=jjMTgMknenBE3/BskYA3h+mUmSjqAFl44ppNbTFyYGA+cJxAe61Yct/X30YHCw2j3G\n QRFy8tfcDlt5+DuNnSk/QvUw1I41UzGiaJSQmf6gnUBeJUuAiD/X5rexBMOTQQuWoiG6\n Lrz/0WRnshSP4/OIgDTvmoZNN2LsTKF0PdrQMYuZEJlpQ8dvWSJMKqN0zYAu0sFrlNL9\n Jilit7XouoOgb6QEj4z75ywgooyEBiLgAeMAbRXyYo1bGw14+SovJhzNVx3GIj7gPmCC\n x+ju6Ppd3RSSJwRgaA+kMUzDlIVtUfjqMGSlq/I5wOARRGcae2ISyjD93hGFtoA54kkT\n cQZA==",
        "X-Gm-Message-State": "AOJu0YyRlUk5NG5zGahLwWIaZb01PIPTatMRYbfotdHJ/NPjd32p6AlX\n kZKMt3Bb1V6O9ZIrSvWpCxzFnRiggih7iolKntp+KGlOoCTlegvYmDqMhXDCG7Cu22uX2MZy/FQ\n n4XYX08ZFKT2XAuhshqh4+GhUEPHHXVI8nueOWbLpyxa9fSWUHi/L6D8vHV5jDn6IzSKjMRaqHw\n copsWBxU/FQy5W6mZfv1UAoEToOw==",
        "X-Google-Smtp-Source": "\n AGHT+IF8bBevTBr+hi72B8EIQHRuo9gXYZ85j1WBQ1yIf0ifW1YrSrQjYFTQ9TgK1PwX1Eskyp0PhA==",
        "X-Received": "by 2002:a81:6c8f:0:b0:5fb:d2da:63d1 with SMTP id\n h137-20020a816c8f000000b005fbd2da63d1mr1414420ywc.36.1705621329192;\n Thu, 18 Jan 2024 15:42:09 -0800 (PST)",
        "From": "Adam Hassick <ahassick@iol.unh.edu>",
        "To": "ci@dpdk.org",
        "Cc": "Adam Hassick <ahassick@iol.unh.edu>",
        "Subject": "[PATCH v3 1/3] tools: Add script to create artifacts",
        "Date": "Thu, 18 Jan 2024 18:41:18 -0500",
        "Message-ID": "<20240118234120.29256-2-ahassick@iol.unh.edu>",
        "X-Mailer": "git-send-email 2.43.0",
        "In-Reply-To": "<20240118234120.29256-1-ahassick@iol.unh.edu>",
        "References": "<20240118234120.29256-1-ahassick@iol.unh.edu>",
        "MIME-Version": "1.0",
        "Content-Transfer-Encoding": "8bit",
        "X-BeenThere": "ci@dpdk.org",
        "X-Mailman-Version": "2.1.29",
        "Precedence": "list",
        "List-Id": "DPDK CI discussions <ci.dpdk.org>",
        "List-Unsubscribe": "<https://mails.dpdk.org/options/ci>,\n <mailto:ci-request@dpdk.org?subject=unsubscribe>",
        "List-Archive": "<http://mails.dpdk.org/archives/ci/>",
        "List-Post": "<mailto:ci@dpdk.org>",
        "List-Help": "<mailto:ci-request@dpdk.org?subject=help>",
        "List-Subscribe": "<https://mails.dpdk.org/listinfo/ci>,\n <mailto:ci-request@dpdk.org?subject=subscribe>",
        "Errors-To": "ci-bounces@dpdk.org"
    },
    "content": "This script takes in a URL to a series on Patchwork and emits a\ntarball which may be used for running tests.\n\nSigned-off-by: Adam Hassick <ahassick@iol.unh.edu>\n---\n tools/create_series_artifact.py | 468 ++++++++++++++++++++++++++++++++\n 1 file changed, 468 insertions(+)\n create mode 100755 tools/create_series_artifact.py",
    "diff": "diff --git a/tools/create_series_artifact.py b/tools/create_series_artifact.py\nnew file mode 100755\nindex 0000000..235896c\n--- /dev/null\n+++ b/tools/create_series_artifact.py\n@@ -0,0 +1,468 @@\n+#!/usr/bin/env python3\n+# SPDX-License-Identifier: BSD-3-Clause\n+# Copyright (c) 2024 University of New Hampshire\n+\n+import argparse\n+from dataclasses import dataclass\n+\n+import os\n+from git_pw import api as pw_api\n+import pathlib\n+import pygit2\n+import requests\n+import shutil\n+import subprocess\n+import tarfile\n+from typing import Any, Dict, List, Optional\n+import yaml\n+\n+HELP = \"\"\"This script will create an artifact given a URL to a Patchwork series.\n+Much of the information provided is acquired by this script through the use of a configuration file.\n+This configuration file can be found with the other script configs in the config directory of the CI repo.\n+This default file is located at \"config/artifacts.yml\" in the dpdk-ci repository.\n+\n+More detail and examples can be found in the doc for this script.\n+\n+Example usage:\n+\n+./create_series_artifact.py ../configs/artifacts.yml https://patches.dpdk.org/api/1.3/series/12345/\n+\n+\"\"\"\n+\n+# Map the outputs of pw_maintainers_cli to the names of branches on the\n+# GitHub mirror. This is temporary, and should be moved elsewhere in\n+# the future.\n+BRANCH_NAME_MAP = {\n+    \"next-baseband\": \"next-baseband-for-main\",\n+    \"next-crypto\": \"next-crypto-for-main\",\n+    \"next-eventdev\": \"next-eventdev-for-main\",\n+    \"next-net\": \"next-net-for-main\",\n+    \"next-net-intel\": \"next-net-intel-for-next-net\",\n+    \"next-net-brcm\": \"next-net-brcm-for-next-net\",\n+    \"next-net-mlx\": \"next-net-mlx-for-next-net\",\n+    \"next-net-mrvl\": \"next-net-mrvl-for-main\",\n+    \"next-virtio\": \"next-virtio-for-next-net\",\n+}\n+\n+\n+@dataclass\n+class CreateSeriesParameters(object):\n+    pw_server: str\n+    pw_project: str\n+    pw_token: str\n+    git_user: str\n+    git_email: str\n+    series_url: str\n+    patch_ids: List[int]\n+    labels: List[str]\n+    config: Dict\n+    series: Dict\n+    pw_mcli_script: pathlib.Path\n+    patch_parser_script: pathlib.Path\n+    patch_parser_cfg: pathlib.Path\n+    lzma: bool\n+    output_tarball: pathlib.Path\n+    output_properties: pathlib.Path\n+    no_depends: bool\n+    docs_only: bool\n+\n+\n+class ProjectTree(object):\n+    artifact_path: pathlib.Path\n+    tree: str\n+    commit_id: str\n+    path: pathlib.Path\n+    log_file_path: pathlib.Path\n+    props_file_path: pathlib.Path\n+    data: CreateSeriesParameters\n+    repo: pygit2.Repository\n+    log_buf: List[str]\n+    properties: Dict[str, Any]\n+\n+    def log(self, msg: str):\n+        print(msg)\n+        self.log_buf.append(msg)\n+\n+    def write_log(self):\n+        with open(self.log_file_path, \"w\") as log_file:\n+            log_file.write(\"\\n\".join([msg for msg in self.log_buf]))\n+\n+    def write_properties(self):\n+        with open(self.props_file_path, \"w\") as prop_file:\n+            for key, value in self.properties.items():\n+                prop_file.write(f\"{key}={value}\\n\")\n+\n+    def move_logs(self):\n+        shutil.move(self.log_file_path, pathlib.Path(os.getcwd(), \"log.txt\"))\n+        shutil.move(\n+            self.props_file_path, pathlib.Path(os.getcwd(), self.data.output_properties)\n+        )\n+\n+    def __init__(self, data: CreateSeriesParameters):\n+        self.data = data\n+        self.path = pathlib.Path(os.curdir, \"dpdk\").absolute()\n+        self.log_buf = []\n+        self.log_file_path = pathlib.Path(self.path, \"log.txt\")\n+        self.props_file_path = pathlib.Path(self.path, data.output_properties)\n+        self.tree = \"main\"\n+        self.properties = {}\n+        self.artifact_path = data.output_tarball\n+\n+        # Set properties related to the patch data.\n+        self.set_properties(\n+            patchset_range=f\"{data.patch_ids[0]}-{data.patch_ids[-1]}\",\n+            tags=\" \".join(data.labels),\n+            is_docs_only=str(data.docs_only),\n+        )\n+\n+        if not self.path.exists():\n+            # Find the URL to clone from based on the tree name.\n+            repo_url = self.data.config[\"repo_url\"]\n+\n+            # Pull down the git repo we found.\n+            for i in range(1, 4):\n+                self.log(f\"Cloning the DPDK mirror at: {repo_url} (Attempt {i} of 3)\")\n+                try:\n+                    repo = pygit2.clone_repository(repo_url, self.path)\n+                    break\n+                except pygit2.GitError as e:\n+                    self.log(f\"Failed! Reason: {e}\")\n+            else:\n+                self.log(\"Failed to clone from the upstream repository.\")\n+                exit(1)\n+        else:\n+            # Fetch any new changes.\n+            repo = pygit2.Repository(self.path)\n+            origin = repo.remotes[\"origin\"]\n+            origin.fetch()\n+\n+            self.log(\"Cleaning repository state...\")\n+            repo.state_cleanup()\n+\n+        # Initially, check out to main.\n+        self.repo = repo\n+        self.checkout(\"main\")\n+\n+        self.log(f\"Done: {self.tree} commit {self.commit_id}\")\n+\n+    def checkout(self, branch: str) -> Optional[str]:\n+        \"\"\"\n+        Check out to some branch.\n+        Returns true if successful, false otherwise.\n+        \"\"\"\n+\n+        git_branch = self.repo.lookup_branch(\n+            f\"origin/{branch}\", pygit2.GIT_BRANCH_REMOTE\n+        )\n+\n+        if not git_branch:\n+            self.log(f\"Tried to checkout to non-existant branch: {branch}\")\n+            return None\n+\n+        self.log(f\"Trying to checkout branch: {git_branch.branch_name}\")\n+        reference = self.repo.resolve_refish(git_branch.branch_name)\n+        self.commit_id = str(reference[0].id)\n+        self.repo.reset(reference[0].id, pygit2.GIT_RESET_HARD)\n+        self.repo.checkout(reference[1])\n+        self.tree = branch\n+\n+        self.log(f\"Checked out to {branch} ({self.commit_id})\")\n+\n+        return branch\n+\n+    def guess_git_tree(self) -> Optional[str]:\n+        \"\"\"\n+        Run pw_maintainers_cli to guess the git tree of the patch series we are applying.\n+        Returns None if the pw_maintainers_cli failed.\n+        \"\"\"\n+\n+        if \"id\" not in self.data.series:\n+            raise Exception(\"ID was not found in the series JSON\")\n+\n+        result = subprocess.run(\n+            [\n+                self.data.pw_mcli_script,\n+                \"--type\",\n+                \"series\",\n+                \"--pw-server\",\n+                self.data.pw_server,\n+                \"--pw-project\",\n+                self.data.pw_project,\n+                \"list-trees\",\n+                str(self.data.series[\"id\"]),\n+            ],\n+            stdout=subprocess.PIPE,\n+            stderr=subprocess.PIPE,\n+            cwd=self.path,\n+            env={\n+                \"MAINTAINERS_FILE_PATH\": \"MAINTAINERS\",\n+                \"PW_TOKEN\": self.data.pw_token,\n+            },\n+        )\n+\n+        if result.returncode == 0:\n+            branch = result.stdout.decode().strip()\n+\n+            if branch in [\"main\", \"dpdk\"]:\n+                branch = \"main\"\n+            else:\n+                if branch.startswith(\"dpdk-\"):\n+                    branch = branch[5:]\n+\n+                branch = BRANCH_NAME_MAP.get(branch)\n+\n+            return self.checkout(branch)\n+        else:\n+            self.log(\"Failed to guess git tree. Output from pw_maintainers_cli:\")\n+            self.log(result.stdout.decode())\n+            self.log(result.stderr.decode())\n+            return None\n+\n+    def set_properties(self, **kwargs):\n+        for key, value in kwargs.items():\n+            self.properties[key] = value\n+\n+    def apply_patch_series(self) -> bool:\n+        self.set_properties(applied_commit_id=self.commit_id, tree=self.tree)\n+        # Run git-pw to apply the series.\n+\n+        # Configure the tree to point at the given patchwork server and project\n+        self.repo.config[\"pw.server\"] = self.data.pw_server\n+        self.repo.config[\"pw.project\"] = self.data.pw_project\n+        self.repo.config[\"user.email\"] = self.data.git_email\n+        self.repo.config[\"user.name\"] = self.data.git_user\n+\n+        result = subprocess.run(\n+            [\"git\", \"pw\", \"series\", \"apply\", str(self.data.series[\"id\"])],\n+            cwd=self.path,\n+            stdout=subprocess.PIPE,\n+            stderr=subprocess.PIPE,\n+        )\n+\n+        # Write the log from the apply process to disk.\n+        self.log(\"Applying patch...\")\n+        self.log(result.stdout.decode())\n+        self.log(result.stderr.decode())\n+\n+        # Store whether there was an error, and return the flag.\n+        error = result.returncode != 0\n+        self.set_properties(apply_error=error)\n+        return not error\n+\n+    def test_build(self) -> bool:\n+        ninja_result: Optional[subprocess.CompletedProcess] = None\n+        meson_result: subprocess.CompletedProcess = subprocess.run(\n+            [\"meson\", \"setup\", \"build\"],\n+            cwd=self.path,\n+            stdout=subprocess.PIPE,\n+            stderr=subprocess.PIPE,\n+        )\n+\n+        build_error = meson_result.returncode != 0\n+\n+        self.log(\"Running test build...\")\n+        self.log(meson_result.stdout.decode())\n+\n+        if not build_error:\n+            ninja_result = subprocess.run(\n+                [\"ninja\", \"-C\", \"build\"],\n+                cwd=self.path,\n+                stdout=subprocess.PIPE,\n+                stderr=subprocess.PIPE,\n+            )\n+\n+            build_error = build_error or ninja_result.returncode != 0\n+            shutil.rmtree(pathlib.Path(self.path, \"build\"))\n+\n+            self.log(ninja_result.stdout.decode())\n+            self.log(ninja_result.stderr.decode())\n+\n+        self.log(meson_result.stderr.decode())\n+\n+        if build_error:\n+            self.log(\"Test build failed.\")\n+\n+        self.set_properties(build_error=build_error)\n+        return not build_error\n+\n+    def create_tarball(self):\n+        # Copy the logs into the artifact tarball.\n+        self.write_log()\n+        self.write_properties()\n+\n+        # Create a tar archive containing the DPDK sources.\n+        with tarfile.open(\n+            self.artifact_path, mode=\"w:xz\" if self.data.lzma else \"w:gz\"\n+        ) as tar_file:\n+            tar_file.add(self.path, \"dpdk\", recursive=True)\n+\n+        # Move the log file out of the working directory.\n+        self.move_logs()\n+\n+        return True\n+\n+\n+def get_tags(\n+    patch_parser_script: pathlib.Path,\n+    patch_parser_cfg: pathlib.Path,\n+    series: Dict,\n+) -> List[str]:\n+    series_filename = f\"{series['id']}.patch\"\n+\n+    # Pull down the patch series as a single file.\n+    pw_api.download(series[\"mbox\"], None, series_filename)\n+\n+    # Call the patch parser script to obtain the tags\n+    parse_result = subprocess.run(\n+        [patch_parser_script, patch_parser_cfg, series_filename],\n+        stdout=subprocess.PIPE,\n+        stderr=subprocess.PIPE,\n+    )\n+\n+    # Assert that patch parser succeeded.\n+    parse_result.check_returncode()\n+\n+    # Return the output\n+    return parse_result.stdout.decode().splitlines()\n+\n+\n+def parse_args() -> argparse.Namespace:\n+    \"\"\"\n+    Parses the arguments and returns an instance of a dataclass containing parameters\n+    and some derived information.\n+    \"\"\"\n+    parser = argparse.ArgumentParser(\n+        formatter_class=argparse.RawDescriptionHelpFormatter,\n+        description=HELP,\n+    )\n+    parser.add_argument(\n+        \"config\",\n+        type=argparse.FileType(),\n+        help=\"The config file to load. Must be a path to a YAML file.\",\n+    )\n+    parser.add_argument(\"series_url\", type=str, help=\"The URL to a Patchwork series.\")\n+    parser.add_argument(\n+        \"-t\",\n+        \"--pw-token\",\n+        dest=\"pw_token\",\n+        type=str,\n+        help=\"The Patchwork token to use\",\n+    )\n+    parser.add_argument(\n+        \"-l\",\n+        \"--lzma\",\n+        action=\"store_true\",\n+        help=\"When set, use LZMA compression rather than GNU zip compression.\",\n+    )\n+    parser.add_argument(\n+        \"-nd\",\n+        \"--no-depends\",\n+        action=\"store_true\",\n+        help=\"When set, does not acknowledge the Depends-on label.\",\n+    )\n+\n+    return parser.parse_args()\n+\n+\n+def collect_series_info(args: argparse.Namespace) -> CreateSeriesParameters:\n+    # Read the configuration file.\n+    with args.config as config_file:\n+        config = yaml.safe_load(config_file)\n+\n+    pw_server = config[\"patchwork\"][\"server\"]\n+    pw_project = config[\"patchwork\"][\"project\"]\n+    pw_token = args.pw_token or config[\"patchwork\"].get(\"token\")\n+\n+    if not pw_token:\n+        print(\"Failed to obtain the Patchworks token.\")\n+        exit(1)\n+\n+    pw_mcli_script = pathlib.Path(config[\"pw_maintainers_cli\"][\"path\"]).absolute()\n+\n+    git_user = config[\"git\"][\"user\"]\n+    git_email = config[\"git\"][\"email\"]\n+\n+    patch_parser_script = pathlib.Path(config[\"patch_parser\"][\"path\"]).absolute()\n+    patch_parser_cfg = pathlib.Path(config[\"patch_parser\"][\"config\"]).absolute()\n+\n+    if args.lzma:\n+        tarball_name = \"dpdk.tar.xz\"\n+    else:\n+        tarball_name = \"dpdk.tar.gz\"\n+\n+    output_tarball = pathlib.Path(tarball_name)\n+    output_properties = pathlib.Path(f\"{tarball_name}.properties\")\n+\n+    # Pull the series JSON down.\n+    resp = requests.get(args.series_url)\n+    resp.raise_for_status()\n+    series = resp.json()\n+\n+    # Get the labels using the patch parser.\n+    labels = get_tags(patch_parser_script, patch_parser_cfg, series)\n+\n+    # See if this is a documentation-only patch.\n+    docs_only = len(labels) == 1 and labels[0] == \"documentation\"\n+\n+    # Get the patch ids in this patch series.\n+    patch_ids = list(map(lambda x: int(x[\"id\"]), series[\"patches\"]))\n+    patch_ids.sort()\n+\n+    return CreateSeriesParameters(\n+        pw_server=pw_server,\n+        pw_project=pw_project,\n+        pw_token=pw_token,\n+        git_user=git_user,\n+        git_email=git_email,\n+        series_url=args.series_url,\n+        patch_ids=patch_ids,\n+        labels=labels,\n+        config=config,\n+        series=series,\n+        pw_mcli_script=pw_mcli_script,\n+        patch_parser_script=patch_parser_script,\n+        patch_parser_cfg=patch_parser_cfg,\n+        lzma=args.lzma,\n+        output_tarball=output_tarball,\n+        output_properties=output_properties,\n+        no_depends=args.no_depends,\n+        docs_only=docs_only,\n+    )\n+\n+\n+def try_to_apply(tree: ProjectTree) -> bool:\n+    return tree.apply_patch_series() and tree.test_build() and tree.create_tarball()\n+\n+\n+def main() -> int:\n+    data = collect_series_info(parse_args())\n+\n+    # Pull down the DPDK mirror.\n+    tree = ProjectTree(data)\n+\n+    # Try to guess the Git tree for this patchset.\n+    guessed_tree = tree.guess_git_tree()\n+\n+    if not guessed_tree:\n+        print(\"Failed to guess git tree.\")\n+        return 1\n+\n+    # Try to apply this patch.\n+    if not (\n+        try_to_apply(tree)  # First, try to apply on the guessed tree.\n+        or guessed_tree != \"main\"  # If that fails, and the guessed tree was not main\n+        and tree.checkout(\"main\")  # Checkout to main, then\n+        and try_to_apply(tree)  # Try to apply on main\n+    ):\n+        tree.write_log()\n+        tree.write_properties()\n+        tree.move_logs()\n+\n+        print(\"FAILURE\")\n+\n+        return 1\n+    return 0\n+\n+\n+if __name__ == \"__main__\":\n+    exit(main())\n",
    "prefixes": [
        "v3",
        "1/3"
    ]
}