Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ Trigger ad-hoc test of specific tree/branch/commit.

- [checkout](checkout)

#### patchset

Test patches on top of an existing checkout.

- [patchset](patchset)

#### testretry

Trigger a test retry for a given Maestro node id.
Expand Down
57 changes: 57 additions & 0 deletions docs/patchset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
+++
title = 'patchset'
date = 2026-07-24T00:00:00+01:00
description = 'Command for testing patches on top of an existing checkout.'
+++

This command tests one or more patches on top of an existing checkout on the
KernelCI Pipeline instance.
It is useful to verify a patch or a patch series before sending it to a
mailing list, or to check whether a fix repairs a failing test.

The patches are applied on top of a checkout node created with the
[checkout](../checkout) command, in the order given.
Patches can be local files, uploaded inline, or URLs from an allowed domain
such as patchwork.kernel.org.

First trigger a checkout of the tree/branch/commit the patches are based on:
```sh
kci-dev checkout --giturl https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git --branch master --tipoftree
```

The command prints a `checkout_nodeid`, which identifies the checkout to
apply the patches on.

Then test local patch files, for example the output of `git format-patch`:
```sh
kci-dev patchset --nodeid <checkout_nodeid> --patch 0001-fix.patch --patch 0002-fix.patch --job-filter "kbuild-gcc-12-x86"
```

Or test a patch series from patchwork:
```sh
kci-dev patchset --nodeid <checkout_nodeid> --patchurl https://patchwork.kernel.org/series/123456/mbox/ --job-filter "kbuild-gcc-12-x86"
```

Where:
- `nodeid` is the id of the checkout node to apply the patches on.
- `patch` is a local patch file to upload. Can be used multiple times.
- `patchurl` is a patch URL from an allowed domain. Can be used multiple
times. `--patch` and `--patchurl` cannot be combined in one request.
- `job-filter` is the job filter to use for the test (optional parameter)
- `platform-filter` is the platform filter to use for the test (optional
parameter)

Patch files must be text-only unified diffs. The pipeline rejects binary
patches, and at most 32 patches of up to 10 MiB each can be uploaded per
request.

### --watch, -w

As with the checkout command, you can use the `--watch` option to watch the
progress of the test on the patched tree, and `--test` to wait for a
particular test result and set the exit code accordingly.
See the [checkout](../checkout) documentation for details.

```sh
kci-dev patchset --nodeid <checkout_nodeid> --patch fix.patch --job-filter baseline-nfs-arm64-qualcomm --watch --test baseline.login
```
5 changes: 5 additions & 0 deletions hugo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ sidebar_cache_limit = 1000
pageRef = "config_file/"
weight = 20

[[menu.main]]
name = "patchset"
pageRef = "patchset/"
weight = 20

[[menu.main]]
name = "testretry"
pageRef = "testretry/"
Expand Down
38 changes: 38 additions & 0 deletions kcidev/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
maestro_get_nodes,
send_checkout_full,
send_jobretry,
send_patchset,
)
from kcidev.main import get_cli

Expand Down Expand Up @@ -568,3 +569,40 @@ def trigger_checkout(
if result is None:
raise KciDevError(f"Maestro checkout failed for {giturl} at {commit}")
return result

def trigger_patchset(
self,
nodeid,
patches=None,
patchurls=None,
job_filter=None,
platform_filter=None,
pipeline_url=None,
token=None,
):
"""Test patches on top of an existing checkout node.

Patches are passed inline as strings via patches, or as URLs from
an allowed domain via patchurls. Exactly one of the two must be
provided.
"""
if bool(patches) == bool(patchurls):
raise KciDevError("Exactly one of patches or patchurls must be provided")
url = pipeline_url or self._instance_setting(
"pipeline", human_readable_key="pipeline URL"
)
token = token or self._instance_setting("token")
result = _as_library_error(
"Maestro patchset failed",
send_patchset,
url,
token,
nodeid,
patches=patches,
patchurls=patchurls,
job_filter=job_filter,
platform_filter=platform_filter,
)
if result is None:
raise KciDevError(f"Maestro patchset failed for node {nodeid}")
return result
69 changes: 63 additions & 6 deletions kcidev/libs/maestro_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def maestro_get_nodes(url, limit, offset, filter, paginate):
return nodes_data


def maestro_check_node(node):
def maestro_check_node(node, root_node="checkout"):
"""
Node can be defined RUNNING/DONE/FAIL based on the state
Simplify, as our current state model suboptimal
Expand All @@ -139,7 +139,7 @@ def maestro_check_node(node):
f"Checking node status - name: {name}, state: {state}, result: {result}"
)

if name == "checkout":
if name == root_node:
if state == "running":
status = "RUNNING"
elif state == "available" or state == "closing":
Expand Down Expand Up @@ -225,10 +225,10 @@ def maestro_node_result(node):
kci_msg(f" - node_id:{node['id']} ({node['updated']})")


def maestro_watch_jobs(baseurl, token, treeid, job_filter, test):
# we need to add to job_filter "checkout" node
def maestro_watch_jobs(baseurl, token, treeid, job_filter, test, root_node="checkout"):
# the tree's root node (checkout or patchset) must complete as well
job_filter = list(job_filter)
job_filter.append("checkout")
job_filter.append(root_node)
kci_log(f"job_filter: {', '.join(job_filter)}")
logging.info(f"Starting job watch for tree {treeid}")
logging.debug(f"Watching jobs: {job_filter}, test: {test}")
Expand Down Expand Up @@ -267,7 +267,7 @@ def maestro_watch_jobs(baseurl, token, treeid, job_filter, test):
test_result = node["result"]
logging.debug(f"Test node {test} result: {test_result}")
if node["name"] in job_filter:
status = maestro_check_node(node)
status = maestro_check_node(node, root_node=root_node)
if status == "DONE":
if job_info[node["name"]]["running"]:
kci_msg("")
Expand Down Expand Up @@ -399,3 +399,60 @@ def send_checkout_full(baseurl, token, **kwargs):
result = response.json()
logging.info(f"Checkout successful - tree ID: {result.get('treeid', 'unknown')}")
return result


def send_patchset(
baseurl,
token,
nodeid,
patches=None,
patchurls=None,
job_filter=None,
platform_filter=None,
):
url = baseurl + "api/patchset"
headers = {
"Content-Type": "application/json; charset=utf-8",
"Authorization": f"{token}",
}
data = {"nodeid": nodeid}
if patches:
data["patch"] = patches
if patchurls:
data["patchurl"] = patchurls
if job_filter:
data["jobfilter"] = job_filter
if platform_filter:
data["platformfilter"] = platform_filter

logging.info(
"Sending patchset request for checkout node %s with %d inline patches "
"and %d patch URLs",
nodeid,
len(patches or []),
len(patchurls or []),
)
safe_data = {key: value for key, value in data.items() if key != "patch"}
if patches:
safe_data["patch"] = [{"bytes": len(p.encode("utf-8"))} for p in patches]
logging.debug("Patchset data: %s", safe_data)

jdata = json.dumps(data)
maestro_print_api_call(url, safe_data)
try:
logging.debug(f"POST request to: {url}")
response = kcidev_session.post(url, headers=headers, data=jdata, timeout=60)
logging.debug(f"Patchset response status: {response.status_code}")
except requests.exceptions.RequestException as e:
logging.error(f"Patchset API request failed: {e}")
kci_err(f"API connection error: {e}")
return None

if response.status_code != 200:
logging.error(f"Patchset failed with status {response.status_code}")
maestro_api_error(response)
return None

result = response.json()
logging.info(f"Patchset submitted: {result.get('message', 'No message')}")
return result
2 changes: 2 additions & 0 deletions kcidev/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
config,
maestro,
mcp,
patchset,
results,
storage,
submit,
Expand Down Expand Up @@ -73,6 +74,7 @@ def register_commands(command_group=None):
command_group.add_command(config.config)
command_group.add_command(maestro.maestro)
command_group.add_command(mcp.mcp)
command_group.add_command(patchset.patchset)
command_group.add_command(testretry.testretry)
command_group.add_command(results.results)
command_group.add_command(storage.storage)
Expand Down
23 changes: 23 additions & 0 deletions kcidev/mcp/tools_maestro.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,26 @@ def trigger_checkout(
return client.trigger_checkout(
giturl, branch, commit, job_filter, platform_filter
)

@server.tool(annotations=action)
@tool_errors
def trigger_patchset(
nodeid: str,
patches: list[str] | None = None,
patchurls: list[str] | None = None,
job_filter: list[str] | None = None,
platform_filter: list[str] | None = None,
):
"""Test patches on top of an existing KernelCI checkout.

Applies patches to the checkout node given by nodeid (for
example one started with trigger_checkout) and runs the
pipeline on the patched tree. Pass unified diff contents
inline in patches, or URLs from an allowed domain such as
patchwork.kernel.org in patchurls; exactly one of the two
must be provided. Returns the new patchset
node; poll progress with list_nodes using 'treeid=<tree id>'.
"""
return client.trigger_patchset(
nodeid, patches, patchurls, job_filter, platform_filter
)
Loading
Loading