mcp-github-crunchtools 1.6.0__tar.gz → 2.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/workflows/constitution.yml +1 -1
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/workflows/dependabot-automerge.yml +1 -1
- mcp_github_crunchtools-2.0.1/.github/workflows/security.yml +68 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.pre-commit-config.yaml +1 -1
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.specify/memory/constitution.md +1 -1
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/CHANGELOG.md +29 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/Containerfile +1 -1
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/PKG-INFO +12 -8
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/README.md +11 -7
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/pyproject.toml +1 -1
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/server.json +2 -2
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/__init__.py +1 -1
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/server.py +23 -11
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/tools/actions.py +98 -22
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/tests/test_tools.py +125 -7
- mcp_github_crunchtools-2.0.1/uv.lock +2594 -0
- mcp_github_crunchtools-1.6.0/.github/workflows/security.yml +0 -140
- mcp_github_crunchtools-1.6.0/uv.lock +0 -2170
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.gatehouse-ignore +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/dependabot.yml +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/workflows/ashigaru.yml +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/workflows/ci.yml +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/workflows/container.yml +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/workflows/gatehouse-retriage.yml +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/workflows/gatehouse.yml +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/workflows/publish.yml +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.gitignore +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.specify/specs/000-baseline/spec.md +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.specify/specs/001-pr-merge-logs-release/spec.md +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.specify/specs/002-request-token-merge-gate/spec.md +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.specify/templates/plan-template.md +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.specify/templates/spec-template.md +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/CLAUDE.md +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/LICENSE +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/SECURITY.md +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/gourmand-exceptions.toml +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/gourmand.toml +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/__main__.py +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/client.py +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/config.py +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/errors.py +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/models.py +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/tools/__init__.py +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/tools/files.py +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/tools/issues.py +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/tools/pull_requests.py +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/tools/releases.py +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/tools/search.py +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/tests/__init__.py +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/tests/conftest.py +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/tests/test_merge_gate.py +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/tests/test_request_token.py +0 -0
- {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/tests/test_validation.py +0 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
name: Security Scan
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
schedule:
|
|
9
|
+
# Run every Monday at 9 AM UTC
|
|
10
|
+
- cron: "0 9 * * 1"
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
issues: write
|
|
16
|
+
pull-requests: write
|
|
17
|
+
security-events: write
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
dependency-audit:
|
|
21
|
+
name: Dependency CVE Scan
|
|
22
|
+
permissions:
|
|
23
|
+
contents: read
|
|
24
|
+
issues: write
|
|
25
|
+
uses: crunchtools/constitution/.github/workflows/dependency-audit.yml@v1.28.0
|
|
26
|
+
|
|
27
|
+
container-scan:
|
|
28
|
+
name: Container Security Scan
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v7
|
|
33
|
+
|
|
34
|
+
- name: Build container image
|
|
35
|
+
run: |
|
|
36
|
+
docker build -f Containerfile -t mcp-github:scan .
|
|
37
|
+
|
|
38
|
+
- name: Run Trivy vulnerability scanner
|
|
39
|
+
uses: aquasecurity/trivy-action@v0.36.0
|
|
40
|
+
with:
|
|
41
|
+
image-ref: "mcp-github:scan"
|
|
42
|
+
format: "sarif"
|
|
43
|
+
output: "trivy-results.sarif"
|
|
44
|
+
severity: "CRITICAL,HIGH"
|
|
45
|
+
|
|
46
|
+
- name: Upload Trivy scan results
|
|
47
|
+
uses: github/codeql-action/upload-sarif@v4
|
|
48
|
+
if: always()
|
|
49
|
+
with:
|
|
50
|
+
sarif_file: "trivy-results.sarif"
|
|
51
|
+
|
|
52
|
+
codeql:
|
|
53
|
+
name: CodeQL Analysis
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/checkout@v7
|
|
58
|
+
|
|
59
|
+
- name: Initialize CodeQL
|
|
60
|
+
uses: github/codeql-action/init@v4
|
|
61
|
+
with:
|
|
62
|
+
languages: python
|
|
63
|
+
queries: security-extended
|
|
64
|
+
|
|
65
|
+
- name: Perform CodeQL Analysis
|
|
66
|
+
uses: github/codeql-action/analyze@v4
|
|
67
|
+
with:
|
|
68
|
+
category: "/language:python"
|
{mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.specify/memory/constitution.md
RENAMED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
> **Ratified:** 2026-06-14
|
|
5
5
|
> **Amended:** 2026-10-14
|
|
6
6
|
> **Status:** Active
|
|
7
|
-
> **Inherits:** [crunchtools/constitution](https://github.com/crunchtools/constitution) v1.
|
|
7
|
+
> **Inherits:** [crunchtools/constitution](https://github.com/crunchtools/constitution) v1.28.0
|
|
8
8
|
> **Profile:** MCP Server
|
|
9
9
|
|
|
10
10
|
This file holds what is specific to mcp-github. The fleet rules and the MCP
|
|
@@ -6,6 +6,35 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.0.1] - 2026-10-31
|
|
10
|
+
|
|
11
|
+
### Security
|
|
12
|
+
|
|
13
|
+
- Upgraded locked dependencies within their declared ranges to clear the
|
|
14
|
+
advisories the fleet dependency audit found (anyio, authlib, cryptography, dnspython, joserfc, opentelemetry-api, pydantic-settings, pyjwt).
|
|
15
|
+
|
|
16
|
+
## [2.0.0] - 2026-10-30
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **Breaking:** `get_failed_job_logs_tool` no longer returns the raw log tail
|
|
21
|
+
(`log`/`log_lines`) by default. Each failed job now carries a
|
|
22
|
+
`failure_summary` instead — the tail lines that name the cause (error
|
|
23
|
+
annotations, test failures, tracebacks, non-zero exits), with
|
|
24
|
+
`summary_lines`. A failed job's log can run to the byte cap and, for a
|
|
25
|
+
repository whose CI quotes attack corpora (e.g. a prompt-injection defense
|
|
26
|
+
project), a scanning gateway may flag and withhold the whole tail, leaving
|
|
27
|
+
the operator unable to see why CI failed. The distilled summary is small and
|
|
28
|
+
signal-only, so it is far more likely to pass such a gateway. Pass
|
|
29
|
+
`include_raw_log=true` to also get the full cleaned tail as `log`/`log_lines`,
|
|
30
|
+
exactly as before (that path is what a gateway may still withhold). The
|
|
31
|
+
summary counts against the same 256 KiB result budget as the raw tail.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- `get_failed_job_logs_tool` gains an `include_raw_log` parameter (default
|
|
36
|
+
`false`).
|
|
37
|
+
|
|
9
38
|
## [1.6.0] - 2026-10-20
|
|
10
39
|
|
|
11
40
|
### Changed
|
|
@@ -27,7 +27,7 @@ FROM quay.io/hummingbird/python:3.14
|
|
|
27
27
|
|
|
28
28
|
# Labels for container metadata
|
|
29
29
|
LABEL name="mcp-github-crunchtools" \
|
|
30
|
-
version="
|
|
30
|
+
version="2.0.1" \
|
|
31
31
|
summary="Secure MCP server for GitHub issues, pull requests, Actions, releases, files, and search" \
|
|
32
32
|
description="A security-focused MCP server for GitHub built on Red Hat UBI" \
|
|
33
33
|
maintainer="crunchtools.com" \
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mcp-github-crunchtools
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: Secure MCP server for GitHub issues, pull requests, Actions, releases, files, and search
|
|
5
5
|
Author: crunchtools.com
|
|
6
6
|
License-Expression: AGPL-3.0-or-later
|
|
@@ -95,7 +95,7 @@ permission the tool needs (see [Token permissions](#token-permissions)).
|
|
|
95
95
|
|------|--------------|------------|
|
|
96
96
|
| `list_workflow_runs_tool` | List workflow runs, filtered by branch or status | Actions: read |
|
|
97
97
|
| `get_workflow_run_tool` | One run's status, conclusion, head SHA and jobs (no logs) | Actions: read |
|
|
98
|
-
| `get_failed_job_logs_tool` |
|
|
98
|
+
| `get_failed_job_logs_tool` | A failure summary (the cause lines) of each failed job in a run; the raw log tail with `include_raw_log` | Actions: read |
|
|
99
99
|
| `trigger_workflow_tool` | Start a fresh run via `workflow_dispatch` (*write*) | Actions: write |
|
|
100
100
|
| `rerun_workflow_run_tool` | Re-run every job of a run (*write*) | Actions: write |
|
|
101
101
|
| `rerun_failed_jobs_tool` | Re-run only the failed jobs of a run (*write*) | Actions: write |
|
|
@@ -130,12 +130,16 @@ permission the tool needs (see [Token permissions](#token-permissions)).
|
|
|
130
130
|
`pagination.next_page` for more.
|
|
131
131
|
- `get_workflow_run_tool` lists up to 300 jobs; `jobs_truncated` says when the
|
|
132
132
|
run has more.
|
|
133
|
-
- `get_failed_job_logs_tool` returns
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
133
|
+
- `get_failed_job_logs_tool` returns a `failure_summary` per job by default —
|
|
134
|
+
the tail lines that name the cause (error annotations, test failures,
|
|
135
|
+
tracebacks, non-zero exits) — scanning at most 1000 lines per job (200 by
|
|
136
|
+
default), for at most 5 failed jobs, and at most 256 KiB of log text in all,
|
|
137
|
+
split evenly between the jobs. Pass `include_raw_log=true` to also get the
|
|
138
|
+
full cleaned tail as `log`/`log_lines`; a scanning gateway may withhold that
|
|
139
|
+
raw tail where it delivers the summary. `tail_lines` in the result is the
|
|
140
|
+
value actually used, `failed_jobs_omitted` counts the jobs left out, and
|
|
141
|
+
`truncated` is set on a job whose log or summary was cut and on the result
|
|
142
|
+
when anything was. Only the first 300 jobs of a run are examined;
|
|
139
143
|
`jobs_truncated` says when the run has more, and the counts then cover only
|
|
140
144
|
those.
|
|
141
145
|
|
|
@@ -69,7 +69,7 @@ permission the tool needs (see [Token permissions](#token-permissions)).
|
|
|
69
69
|
|------|--------------|------------|
|
|
70
70
|
| `list_workflow_runs_tool` | List workflow runs, filtered by branch or status | Actions: read |
|
|
71
71
|
| `get_workflow_run_tool` | One run's status, conclusion, head SHA and jobs (no logs) | Actions: read |
|
|
72
|
-
| `get_failed_job_logs_tool` |
|
|
72
|
+
| `get_failed_job_logs_tool` | A failure summary (the cause lines) of each failed job in a run; the raw log tail with `include_raw_log` | Actions: read |
|
|
73
73
|
| `trigger_workflow_tool` | Start a fresh run via `workflow_dispatch` (*write*) | Actions: write |
|
|
74
74
|
| `rerun_workflow_run_tool` | Re-run every job of a run (*write*) | Actions: write |
|
|
75
75
|
| `rerun_failed_jobs_tool` | Re-run only the failed jobs of a run (*write*) | Actions: write |
|
|
@@ -104,12 +104,16 @@ permission the tool needs (see [Token permissions](#token-permissions)).
|
|
|
104
104
|
`pagination.next_page` for more.
|
|
105
105
|
- `get_workflow_run_tool` lists up to 300 jobs; `jobs_truncated` says when the
|
|
106
106
|
run has more.
|
|
107
|
-
- `get_failed_job_logs_tool` returns
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
- `get_failed_job_logs_tool` returns a `failure_summary` per job by default —
|
|
108
|
+
the tail lines that name the cause (error annotations, test failures,
|
|
109
|
+
tracebacks, non-zero exits) — scanning at most 1000 lines per job (200 by
|
|
110
|
+
default), for at most 5 failed jobs, and at most 256 KiB of log text in all,
|
|
111
|
+
split evenly between the jobs. Pass `include_raw_log=true` to also get the
|
|
112
|
+
full cleaned tail as `log`/`log_lines`; a scanning gateway may withhold that
|
|
113
|
+
raw tail where it delivers the summary. `tail_lines` in the result is the
|
|
114
|
+
value actually used, `failed_jobs_omitted` counts the jobs left out, and
|
|
115
|
+
`truncated` is set on a job whose log or summary was cut and on the result
|
|
116
|
+
when anything was. Only the first 300 jobs of a run are examined;
|
|
113
117
|
`jobs_truncated` says when the run has more, and the counts then cover only
|
|
114
118
|
those.
|
|
115
119
|
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/crunchtools/mcp-github",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "
|
|
10
|
+
"version": "2.0.1",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "mcp-github-crunchtools",
|
|
15
|
-
"version": "
|
|
15
|
+
"version": "2.0.1",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|
{mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/server.py
RENAMED
|
@@ -46,7 +46,7 @@ READ_ONLY = {"readOnlyHint": True}
|
|
|
46
46
|
|
|
47
47
|
mcp = FastMCP(
|
|
48
48
|
name="mcp-github",
|
|
49
|
-
version="
|
|
49
|
+
version="2.0.1",
|
|
50
50
|
instructions=(
|
|
51
51
|
"Secure MCP server for GitHub repositories: issues, pull requests "
|
|
52
52
|
"(open, review discussion, diffs, CI checks, merge), Actions runs and "
|
|
@@ -713,33 +713,45 @@ async def get_failed_job_logs_tool(
|
|
|
713
713
|
run_id: Annotated[int, Field(ge=1)],
|
|
714
714
|
tail_lines: int = 200,
|
|
715
715
|
owner: str | None = None,
|
|
716
|
+
include_raw_log: bool = False,
|
|
716
717
|
) -> dict[str, Any]:
|
|
717
|
-
"""Get the
|
|
718
|
+
"""Get the failure summary of each failed job in a GitHub Actions run.
|
|
718
719
|
|
|
719
|
-
Returns
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
720
|
+
Returns, for every job that failed or timed out, a ``failure_summary``: the
|
|
721
|
+
log-tail lines that name the cause (error annotations, test failures,
|
|
722
|
+
tracebacks, non-zero exits), with ANSI escapes and timestamps removed, plus
|
|
723
|
+
the names of the steps that failed. Set ``include_raw_log`` to also get the
|
|
724
|
+
full cleaned tail as ``log`` — it can be large and, for a repository whose
|
|
725
|
+
CI output quotes attack corpora, a scanning gateway may flag and withhold
|
|
726
|
+
it, where the small, signal-only summary is far more likely to pass.
|
|
727
|
+
|
|
728
|
+
Logs are fetched for at most 5 failed jobs and all log text together is
|
|
729
|
+
capped at 256 KiB; ``truncated`` and ``failed_jobs_omitted`` say when
|
|
730
|
+
something was cut. ``jobs_truncated`` means the run has more than the 300
|
|
731
|
+
jobs examined, so the failed-job counts cover only those.
|
|
725
732
|
|
|
726
733
|
Args:
|
|
727
734
|
repo: Repository name
|
|
728
735
|
run_id: Workflow run ID
|
|
729
|
-
tail_lines: Lines to
|
|
730
|
-
max 1000)
|
|
736
|
+
tail_lines: Lines of the tail to scan for the summary, and to return as
|
|
737
|
+
``log`` when ``include_raw_log`` is set (default 200, max 1000)
|
|
731
738
|
owner: Repository owner (defaults to GITHUB_DEFAULT_ORG if unset)
|
|
739
|
+
include_raw_log: Also return the full cleaned tail as ``log`` (default
|
|
740
|
+
False); the raw tail may be withheld by a scanning gateway, the
|
|
741
|
+
summary is not
|
|
732
742
|
|
|
733
743
|
Returns:
|
|
734
744
|
{"run_id", "tail_lines", "failed_job_count", "failed_jobs_omitted",
|
|
735
745
|
"jobs_truncated", "truncated", "jobs": [{"id", "name", "conclusion",
|
|
736
|
-
"html_url", "failed_steps", "
|
|
746
|
+
"html_url", "failed_steps", "failure_summary", "summary_lines",
|
|
747
|
+
"truncated", and "log"/"log_lines" when include_raw_log}]}
|
|
737
748
|
"""
|
|
738
749
|
return await get_failed_job_logs(
|
|
739
750
|
owner=owner,
|
|
740
751
|
repo=repo,
|
|
741
752
|
run_id=run_id,
|
|
742
753
|
tail_lines=tail_lines,
|
|
754
|
+
include_raw_log=include_raw_log,
|
|
743
755
|
)
|
|
744
756
|
|
|
745
757
|
|
|
@@ -306,38 +306,101 @@ def _tail_log(download: dict[str, Any], tail_lines: int, max_bytes: int) -> dict
|
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
|
|
309
|
+
_SUMMARY_MAX_LINES = 60
|
|
310
|
+
_SUMMARY_MAX_BYTES = 16 * 1024
|
|
311
|
+
|
|
312
|
+
#: Lines that carry a failure's cause rather than the output around it. A failed
|
|
313
|
+
#: job's log can run to the byte cap and, in a security repo, quote attack
|
|
314
|
+
#: corpora verbatim; the distilled summary is what an operator needs to see the
|
|
315
|
+
#: cause and is small enough that a scanning gateway delivers it, where the raw
|
|
316
|
+
#: tail is flagged and withheld. Each cleaned line is tested on its own, so ``^``
|
|
317
|
+
#: anchors that line.
|
|
318
|
+
_FAILURE_SIGNAL = re.compile(
|
|
319
|
+
r"(?i)"
|
|
320
|
+
r"\#\#\[error\]|::error|" # GitHub Actions error annotations
|
|
321
|
+
r"\bFAIL(?:ED|URE)?\b|\bERRORS?\b|" # test/build failure words
|
|
322
|
+
r"^E\s|" # pytest assertion detail line
|
|
323
|
+
r"Traceback \(most recent call last\)|"
|
|
324
|
+
r"\b\w*(?:Error|Exception):|\bassert\b|"
|
|
325
|
+
r"short test summary|={3,} FAILURES|={3,} ERRORS|"
|
|
326
|
+
r"Process completed with exit code|exited with (?:code )?\d|"
|
|
327
|
+
r"npm ERR!|make: \*\*\*"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _failure_summary(log_text: str) -> dict[str, Any]:
|
|
332
|
+
"""Pull the failure-signal lines out of a cleaned log tail.
|
|
333
|
+
|
|
334
|
+
Keeps only the lines that name the cause (error annotations, test failures,
|
|
335
|
+
tracebacks, non-zero exits), newest first dropped last, capped at
|
|
336
|
+
``_SUMMARY_MAX_LINES`` and ``_SUMMARY_MAX_BYTES``. This is what a caller gets
|
|
337
|
+
by default; the raw tail is behind ``include_raw_log`` because it can run
|
|
338
|
+
long and, for a security repo, quotes attack payloads a scanning gateway
|
|
339
|
+
may flag and withhold. Being small and signal-only, the summary is far more
|
|
340
|
+
likely to pass such a gateway than the raw tail; whether it does is the
|
|
341
|
+
gateway's call, not this tool's.
|
|
342
|
+
"""
|
|
343
|
+
hits = [line for line in log_text.split("\n") if line and _FAILURE_SIGNAL.search(line)]
|
|
344
|
+
kept = hits[-_SUMMARY_MAX_LINES:]
|
|
345
|
+
truncated = len(kept) < len(hits)
|
|
346
|
+
text = "\n".join(kept)
|
|
347
|
+
|
|
348
|
+
encoded = text.encode()
|
|
349
|
+
if len(encoded) > _SUMMARY_MAX_BYTES:
|
|
350
|
+
truncated = True
|
|
351
|
+
kept_bytes = encoded[-_SUMMARY_MAX_BYTES:].decode("utf-8", errors="ignore")
|
|
352
|
+
text = kept_bytes.partition("\n")[2] or kept_bytes
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
"failure_summary": text,
|
|
356
|
+
"summary_lines": text.count("\n") + 1 if text else 0,
|
|
357
|
+
"summary_truncated": truncated,
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
309
361
|
async def get_failed_job_logs(
|
|
310
362
|
owner: str | None,
|
|
311
363
|
repo: str,
|
|
312
364
|
run_id: int,
|
|
313
365
|
tail_lines: int = DEFAULT_LOG_TAIL_LINES,
|
|
366
|
+
include_raw_log: bool = False,
|
|
314
367
|
) -> dict[str, Any]:
|
|
315
368
|
"""Get the end of the log of each failed job in a workflow run.
|
|
316
369
|
|
|
317
370
|
A job counts as failed when its conclusion is "failure" or "timed_out".
|
|
318
|
-
For each,
|
|
319
|
-
|
|
320
|
-
|
|
371
|
+
For each, a ``failure_summary`` is returned by default: the lines of the log
|
|
372
|
+
tail that name the cause (error annotations, test failures, tracebacks,
|
|
373
|
+
non-zero exits), with ANSI escapes and per-line timestamps removed, along
|
|
374
|
+
with the names of the steps that failed. The full ``tail_lines``-line tail
|
|
375
|
+
is returned as ``log`` only when ``include_raw_log`` is set — it can run to
|
|
376
|
+
the byte cap and, for a repository whose CI quotes attack corpora, a
|
|
377
|
+
scanning gateway may flag and withhold it, where the small, signal-only
|
|
378
|
+
summary is far more likely to pass.
|
|
321
379
|
|
|
322
380
|
Limits, each reported in the result when it applies: ``tail_lines`` is
|
|
323
381
|
capped at 1000 (the value used is returned); logs are fetched for at most
|
|
324
382
|
5 failed jobs (``failed_jobs_omitted`` counts the rest); and all log text
|
|
325
383
|
together stays within 256 KiB, split evenly between the jobs (a job whose
|
|
326
|
-
log was cut
|
|
327
|
-
|
|
328
|
-
|
|
384
|
+
log or summary was cut has ``truncated`` set). Only the first 300 jobs of a
|
|
385
|
+
run are examined: when ``jobs_truncated`` is set the run has more, and the
|
|
386
|
+
failed-job counts cover only those examined.
|
|
329
387
|
|
|
330
388
|
Args:
|
|
331
389
|
owner: Repository owner (defaults to GITHUB_DEFAULT_ORG if unset)
|
|
332
390
|
repo: Repository name
|
|
333
391
|
run_id: Workflow run ID
|
|
334
|
-
tail_lines: Lines
|
|
392
|
+
tail_lines: Lines of the raw tail to scan for the summary, and to
|
|
393
|
+
return as ``log`` when ``include_raw_log`` is set (default 200,
|
|
335
394
|
max 1000)
|
|
395
|
+
include_raw_log: Also return the full cleaned tail as ``log`` (default
|
|
396
|
+
False). The raw tail may be withheld by a scanning gateway; the
|
|
397
|
+
summary is not.
|
|
336
398
|
|
|
337
399
|
Returns:
|
|
338
400
|
{"run_id", "tail_lines", "failed_job_count", "failed_jobs_omitted",
|
|
339
401
|
"jobs_truncated", "truncated", "jobs": [{"id", "name", "conclusion",
|
|
340
|
-
"html_url", "failed_steps", "
|
|
402
|
+
"html_url", "failed_steps", "failure_summary", "summary_lines",
|
|
403
|
+
"truncated", and "log"/"log_lines" when include_raw_log}]}
|
|
341
404
|
"""
|
|
342
405
|
owner = resolve_owner(owner)
|
|
343
406
|
repo = validate_name(repo, "repo")
|
|
@@ -358,20 +421,33 @@ async def get_failed_job_logs(
|
|
|
358
421
|
f"/repos/{owner}/{repo}/actions/jobs/{job_id}/logs",
|
|
359
422
|
tail_bytes=MAX_LOG_TOTAL_BYTES,
|
|
360
423
|
)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
424
|
+
# One 256 KiB budget covers all log text, split evenly between jobs. The
|
|
425
|
+
# summary is log text too, so when the raw tail is also returned the
|
|
426
|
+
# summary's allowance comes out of the same per-job share, keeping the
|
|
427
|
+
# pair within budget (the summary has priority; the raw tail takes what
|
|
428
|
+
# is left).
|
|
429
|
+
per_job = MAX_LOG_TOTAL_BYTES // len(selected)
|
|
430
|
+
raw_budget = max(per_job - _SUMMARY_MAX_BYTES, 0) if include_raw_log else per_job
|
|
431
|
+
tail = _tail_log(download, tail_lines, raw_budget)
|
|
432
|
+
summary = _failure_summary(tail["log"])
|
|
433
|
+
entry: dict[str, Any] = {
|
|
434
|
+
"id": job_id,
|
|
435
|
+
"name": job.get("name"),
|
|
436
|
+
"conclusion": job.get("conclusion"),
|
|
437
|
+
"html_url": job.get("html_url"),
|
|
438
|
+
"failed_steps": [
|
|
439
|
+
step.get("name")
|
|
440
|
+
for step in job.get("steps") or []
|
|
441
|
+
if step.get("conclusion") in FAILED_JOB_CONCLUSIONS
|
|
442
|
+
],
|
|
443
|
+
"failure_summary": summary["failure_summary"],
|
|
444
|
+
"summary_lines": summary["summary_lines"],
|
|
445
|
+
"truncated": tail["truncated"] or summary["summary_truncated"],
|
|
446
|
+
}
|
|
447
|
+
if include_raw_log:
|
|
448
|
+
entry["log"] = tail["log"]
|
|
449
|
+
entry["log_lines"] = tail["log_lines"]
|
|
450
|
+
results.append(entry)
|
|
375
451
|
|
|
376
452
|
failed_jobs_omitted = len(failed) - len(selected)
|
|
377
453
|
return {
|