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.
Files changed (56) hide show
  1. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/workflows/constitution.yml +1 -1
  2. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/workflows/dependabot-automerge.yml +1 -1
  3. mcp_github_crunchtools-2.0.1/.github/workflows/security.yml +68 -0
  4. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.pre-commit-config.yaml +1 -1
  5. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.specify/memory/constitution.md +1 -1
  6. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/CHANGELOG.md +29 -0
  7. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/Containerfile +1 -1
  8. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/PKG-INFO +12 -8
  9. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/README.md +11 -7
  10. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/pyproject.toml +1 -1
  11. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/server.json +2 -2
  12. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/__init__.py +1 -1
  13. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/server.py +23 -11
  14. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/tools/actions.py +98 -22
  15. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/tests/test_tools.py +125 -7
  16. mcp_github_crunchtools-2.0.1/uv.lock +2594 -0
  17. mcp_github_crunchtools-1.6.0/.github/workflows/security.yml +0 -140
  18. mcp_github_crunchtools-1.6.0/uv.lock +0 -2170
  19. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.gatehouse-ignore +0 -0
  20. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  21. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  22. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  23. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/dependabot.yml +0 -0
  24. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/workflows/ashigaru.yml +0 -0
  25. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/workflows/ci.yml +0 -0
  26. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/workflows/container.yml +0 -0
  27. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/workflows/gatehouse-retriage.yml +0 -0
  28. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/workflows/gatehouse.yml +0 -0
  29. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.github/workflows/publish.yml +0 -0
  30. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.gitignore +0 -0
  31. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.specify/specs/000-baseline/spec.md +0 -0
  32. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.specify/specs/001-pr-merge-logs-release/spec.md +0 -0
  33. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.specify/specs/002-request-token-merge-gate/spec.md +0 -0
  34. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.specify/templates/plan-template.md +0 -0
  35. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/.specify/templates/spec-template.md +0 -0
  36. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/CLAUDE.md +0 -0
  37. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/LICENSE +0 -0
  38. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/SECURITY.md +0 -0
  39. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/gourmand-exceptions.toml +0 -0
  40. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/gourmand.toml +0 -0
  41. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/__main__.py +0 -0
  42. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/client.py +0 -0
  43. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/config.py +0 -0
  44. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/errors.py +0 -0
  45. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/models.py +0 -0
  46. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/tools/__init__.py +0 -0
  47. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/tools/files.py +0 -0
  48. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/tools/issues.py +0 -0
  49. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/tools/pull_requests.py +0 -0
  50. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/tools/releases.py +0 -0
  51. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/src/mcp_github_crunchtools/tools/search.py +0 -0
  52. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/tests/__init__.py +0 -0
  53. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/tests/conftest.py +0 -0
  54. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/tests/test_merge_gate.py +0 -0
  55. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/tests/test_request_token.py +0 -0
  56. {mcp_github_crunchtools-1.6.0 → mcp_github_crunchtools-2.0.1}/tests/test_validation.py +0 -0
@@ -18,4 +18,4 @@ permissions:
18
18
  jobs:
19
19
  constitution:
20
20
  name: Constitution
21
- uses: crunchtools/constitution/.github/workflows/validate.yml@v1.24.0
21
+ uses: crunchtools/constitution/.github/workflows/validate.yml@v1.28.0
@@ -15,4 +15,4 @@ permissions:
15
15
  jobs:
16
16
  automerge:
17
17
  name: Dependabot
18
- uses: crunchtools/constitution/.github/workflows/dependabot-automerge.yml@v1.24.0
18
+ uses: crunchtools/constitution/.github/workflows/dependabot-automerge.yml@v1.28.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"
@@ -12,7 +12,7 @@ repos:
12
12
  types: [python]
13
13
  stages: [pre-commit, pre-merge-commit]
14
14
  - repo: https://github.com/crunchtools/constitution
15
- rev: v1.24.0
15
+ rev: v1.28.0
16
16
  hooks:
17
17
  - id: validate-constitution
18
18
 
@@ -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.24.0
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="1.6.0" \
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: 1.6.0
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` | The last lines of the log of each failed job in a run, with ANSI escapes and timestamps removed | Actions: read |
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 at most 1000 lines per job (200 by
134
- default), for at most 5 failed jobs, and at most 256 KiB of log text in
135
- all, split evenly between the jobs. `tail_lines` in the result is the value
136
- actually used, `failed_jobs_omitted` counts the jobs left out, and
137
- `truncated` is set on a job whose log was cut short and on the result when
138
- anything was. Only the first 300 jobs of a run are examined;
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` | The last lines of the log of each failed job in a run, with ANSI escapes and timestamps removed | Actions: read |
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 at most 1000 lines per job (200 by
108
- default), for at most 5 failed jobs, and at most 256 KiB of log text in
109
- all, split evenly between the jobs. `tail_lines` in the result is the value
110
- actually used, `failed_jobs_omitted` counts the jobs left out, and
111
- `truncated` is set on a job whose log was cut short and on the result when
112
- anything was. Only the first 300 jobs of a run are examined;
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcp-github-crunchtools"
3
- version = "1.6.0"
3
+ version = "2.0.1"
4
4
  description = "Secure MCP server for GitHub issues, pull requests, Actions, releases, files, and search"
5
5
  requires-python = ">=3.10"
6
6
  license = "AGPL-3.0-or-later"
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/crunchtools/mcp-github",
8
8
  "source": "github"
9
9
  },
10
- "version": "1.6.0",
10
+ "version": "2.0.1",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "mcp-github-crunchtools",
15
- "version": "1.6.0",
15
+ "version": "2.0.1",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }
@@ -29,7 +29,7 @@ import argparse
29
29
 
30
30
  from .server import mcp
31
31
 
32
- __version__ = "1.6.0"
32
+ __version__ = "2.0.1"
33
33
  __all__ = ["main", "mcp"]
34
34
 
35
35
 
@@ -46,7 +46,7 @@ READ_ONLY = {"readOnlyHint": True}
46
46
 
47
47
  mcp = FastMCP(
48
48
  name="mcp-github",
49
- version="1.6.0",
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 end of the log of each failed job in a GitHub Actions run.
718
+ """Get the failure summary of each failed job in a GitHub Actions run.
718
719
 
719
- Returns the last ``tail_lines`` lines of every job that failed or timed
720
- out, with ANSI escapes and timestamps removed, and the names of the steps
721
- that failed. Logs are fetched for at most 5 failed jobs and all log text
722
- together is capped at 256 KiB; ``truncated`` and ``failed_jobs_omitted``
723
- say when something was cut. ``jobs_truncated`` means the run has more than
724
- the 300 jobs examined, so the failed-job counts cover only those.
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 return from the end of each log (default 200,
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", "log", "log_lines", "truncated"}]}
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, the last ``tail_lines`` lines of its log are returned with ANSI
319
- escapes and the per-line timestamps removed, along with the names of the
320
- steps that failed.
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 short of ``tail_lines`` has ``truncated`` set). Only the first
327
- 300 jobs of a run are examined: when ``jobs_truncated`` is set the run has
328
- more, and the failed-job counts cover only those examined.
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 to return from the end of each log (default 200,
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", "log", "log_lines", "truncated"}]}
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
- results.append(
362
- {
363
- "id": job_id,
364
- "name": job.get("name"),
365
- "conclusion": job.get("conclusion"),
366
- "html_url": job.get("html_url"),
367
- "failed_steps": [
368
- step.get("name")
369
- for step in job.get("steps") or []
370
- if step.get("conclusion") in FAILED_JOB_CONCLUSIONS
371
- ],
372
- **_tail_log(download, tail_lines, MAX_LOG_TOTAL_BYTES // len(selected)),
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 {