From patchwork Sun Feb 22 15:42:53 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 1934 Return-Path: X-Original-To: u-boot-concept@u-boot.org Delivered-To: u-boot-concept@u-boot.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1771775072; bh=TLZmzONQdWAZLI26VtaI13n4c/r8z+m2uoJOwefIpXA=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=nTSwNK8rTIaZalBga5pZ+kdFLb0mIYLEGxjEDQMuZdEf4NlX51GvcBWFx90TUj9vn wqHUx08SSnt4WxqrbIcd6/D3BboOmJ5yRM3X35Pdi3lzz2+wIs58CAalLDY0W6Oa5a ZeSQn3kLiVeVaTXlnH14JqnOvz086QsdTX3oxLGVlwwB3Che+djJgwjVDrzhxG4NL+ QzvhnbaWGInZQRVLV2ni7kEMVIDDOzBXQ2xalwucRijumcKOZnCbOedK0342VjfzLp VK1JULKW1CEYGDO0WDeLCLnr3WckOJy39En/0V2je5aqlvC91eD/t6PA1FuCizhXcc wTjrIExCS1D8g== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id A279669D3F for ; Sun, 22 Feb 2026 08:44:32 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id I24KDf0aiRlO for ; Sun, 22 Feb 2026 08:44:32 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1771775072; bh=TLZmzONQdWAZLI26VtaI13n4c/r8z+m2uoJOwefIpXA=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=nTSwNK8rTIaZalBga5pZ+kdFLb0mIYLEGxjEDQMuZdEf4NlX51GvcBWFx90TUj9vn wqHUx08SSnt4WxqrbIcd6/D3BboOmJ5yRM3X35Pdi3lzz2+wIs58CAalLDY0W6Oa5a ZeSQn3kLiVeVaTXlnH14JqnOvz086QsdTX3oxLGVlwwB3Che+djJgwjVDrzhxG4NL+ QzvhnbaWGInZQRVLV2ni7kEMVIDDOzBXQ2xalwucRijumcKOZnCbOedK0342VjfzLp VK1JULKW1CEYGDO0WDeLCLnr3WckOJy39En/0V2je5aqlvC91eD/t6PA1FuCizhXcc wTjrIExCS1D8g== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 8BFFA69D48 for ; Sun, 22 Feb 2026 08:44:32 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1771775070; bh=pi/IppJZBKTcUyb15iAkRt9hDQG1FS/x63mUYqfIzmI=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=lW6VnosRcT3/VNVMNYVm5Z7tu40qYHKNR0pyagBTWlMOGyBJdP9afEOun1heFUogt QGQT/6xfkP+Mxc/ImbS34kVtJUYQ58SZed9jyd9lFk/qNBrJxrLLIqWKcBqUxb73ed H+YzJDbyc8R7o/ifQmh10S+smcjPTtiwUMeIr4r/QTukrHeWN9oJ68OnyI0SBQtgrF csY2eYPuv79lgsBk2qAGFwiLL8J9gE0Vl9V/kbKK1SLONSXaonDjZvCbMb8Qbn++tM WEXC8Hc4Zhmi4yIK9f+epABnyZHZG5SJIy10w1D5owJrt+iOfIxu9mWf0cbd3mIOlX 09MYk58+zXxMA== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 1297C69D3F; Sun, 22 Feb 2026 08:44:30 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10026) with ESMTP id 5j7z1YeyLVWf; Sun, 22 Feb 2026 08:44:29 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1771775066; bh=ulOHxx/hYtAw46cfcYI6LcXFJFrVnNLam2DClBxShfk=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=cmKPlqa2spEH3DkjpIGWkxRd+6JfF/s4M2anfubpE11cjv/l0LukaAEWEWr+b3qS5 HVElDGcXMiiB++PX96cBr7XWBitxnfh/ssDGMgGFmImdZPoPqorw816542vX6cBUFE Um/6CzZAxM8LCVK3uaL99o4WeIK/aSds1MpO7bsSF225oZEnrRZr5uFRfsrvO/ATI4 atDCGuGJYYPadd0cAlEI1/dT+Yo/67uNxX8YfORVI9agYsrMkf0fjZZ6C+BbZGkBlC RqFJVCJ+lynIECX9R5hswXwlg2yhM9kWHW7nbaWt3ed8uj+Qurt5UxMZ0LknYj1ARQ OtB3roCSlBTeQ== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id B4B5469C5C; Sun, 22 Feb 2026 08:44:25 -0700 (MST) From: Simon Glass To: U-Boot Concept Date: Sun, 22 Feb 2026 08:42:53 -0700 Message-ID: <20260222154303.2851319-14-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260222154303.2851319-1-sjg@u-boot.org> References: <20260222154303.2851319-1-sjg@u-boot.org> MIME-Version: 1.0 Message-ID-Hash: BVVXBF4RTZ64MTBMEL7KKUHPWGVFHCV5 X-Message-ID-Hash: BVVXBF4RTZ64MTBMEL7KKUHPWGVFHCV5 X-MailFrom: sjg@u-boot.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Simon Glass , "Claude Opus 4 . 6" X-Mailman-Version: 3.3.10 Precedence: list Subject: [Concept] [PATCH 13/16] pickman: Add pipeline helpers to gitlab_api List-Id: Discussion and patches related to U-Boot Concept Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: From: Simon Glass Add PickmanMr pipeline_status/pipeline_id fields (extracted from the head_pipeline attribute), PipelineInfo and FailedJob named tuples, and get_failed_jobs() which fetches the trace logs of failed jobs from a GitLab pipeline. Co-developed-by: Claude Opus 4.6 Signed-off-by: Simon Glass --- tools/pickman/gitlab_api.py | 77 ++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/tools/pickman/gitlab_api.py b/tools/pickman/gitlab_api.py index 9262ac15a0d..d9635d392ee 100644 --- a/tools/pickman/gitlab_api.py +++ b/tools/pickman/gitlab_api.py @@ -42,14 +42,21 @@ class MrCreateError(Exception): # Use defaults for new fields so existing code doesn't break PickmanMr = namedtuple('PickmanMr', [ 'iid', 'title', 'web_url', 'source_branch', 'description', - 'has_conflicts', 'needs_rebase' -], defaults=[False, False]) + 'has_conflicts', 'needs_rebase', 'pipeline_status', 'pipeline_id' +], defaults=[False, False, None, None]) # Comment info returned by get_mr_comments() MrComment = namedtuple('MrComment', [ 'id', 'author', 'body', 'created_at', 'resolvable', 'resolved' ]) +# Pipeline info +PipelineInfo = namedtuple('PipelineInfo', ['id', 'status', 'web_url']) + +# Failed job info from a pipeline +FailedJob = namedtuple('FailedJob', + ['id', 'name', 'stage', 'web_url', 'log_tail']) + def check_available(): """Check if the python-gitlab module is available @@ -320,6 +327,9 @@ def get_pickman_mrs(remote, state='opened'): # For open MRs, fetch full details since list() doesn't # include accurate merge status fields + pipeline_status = None + pipeline_id = None + if state == 'opened': full_mr = project.mergerequests.get(merge_req.iid) has_conflicts = getattr(full_mr, 'has_conflicts', False) @@ -333,6 +343,12 @@ def get_pickman_mrs(remote, state='opened'): diverged = getattr(full_mr, 'diverged_commits_count', 0) needs_rebase = diverged and diverged > 0 + # Extract pipeline info from head_pipeline + head_pipeline = getattr(full_mr, 'head_pipeline', None) + if head_pipeline: + pipeline_status = head_pipeline.get('status') + pipeline_id = head_pipeline.get('id') + pickman_mrs.append(PickmanMr( iid=merge_req.iid, title=merge_req.title, @@ -341,6 +357,8 @@ def get_pickman_mrs(remote, state='opened'): description=merge_req.description or '', has_conflicts=has_conflicts, needs_rebase=needs_rebase, + pipeline_status=pipeline_status, + pipeline_id=pipeline_id, )) return pickman_mrs except gitlab.exceptions.GitlabError as exc: @@ -423,6 +441,61 @@ def get_mr_comments(remote, mr_iid): return None +def get_failed_jobs(remote, pipeline_id, max_log_lines=200): + """Get failed jobs from a pipeline + + Args: + remote (str): Remote name + pipeline_id (int): Pipeline ID + max_log_lines (int): Maximum log lines to fetch per job + + Returns: + list: List of FailedJob tuples, or None on failure + """ + if not check_available(): + return None + + token = get_token() + if not token: + tout.error('GITLAB_TOKEN environment variable not set') + return None + + remote_url = get_remote_url(remote) + host, proj_path = parse_url(remote_url) + + if not host or not proj_path: + return None + + try: + glab = gitlab.Gitlab(f'https://{host}', private_token=token) + project = glab.projects.get(proj_path) + pipeline = project.pipelines.get(pipeline_id) + jobs = pipeline.jobs.list(scope='failed', get_all=True) + + failed_jobs = [] + for job in jobs: + # Fetch full job to get trace + full_job = project.jobs.get(job.id) + try: + trace = full_job.trace().decode('utf-8', errors='replace') + lines = trace.splitlines() + log_tail = '\n'.join(lines[-max_log_lines:]) + except (AttributeError, gitlab.exceptions.GitlabError): + log_tail = '' + + failed_jobs.append(FailedJob( + id=job.id, + name=job.name, + stage=job.stage, + web_url=job.web_url, + log_tail=log_tail, + )) + return failed_jobs + except gitlab.exceptions.GitlabError as exc: + tout.error(f'GitLab API error: {exc}') + return None + + def reply_to_mr(remote, mr_iid, message): """Post a reply to a merge request