workledger-cli 0.7.0__tar.gz → 0.8.0__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.
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.github/workflows/tests.yml +12 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.gitignore +3 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/CHANGELOG.md +11 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/PKG-INFO +3 -1
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/README.md +2 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/pyproject.toml +1 -1
- workledger_cli-0.8.0/scripts/sonar-pr-issues.py +168 -0
- workledger_cli-0.8.0/tests/test_ledger.py +440 -0
- workledger_cli-0.8.0/tests/test_sonar_pr_issues.py +128 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/uv.lock +2 -2
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/workledger/cli/__init__.py +4 -0
- workledger_cli-0.8.0/workledger/cli/_commands/_store.py +292 -0
- workledger_cli-0.8.0/workledger/cli/_commands/append.py +66 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/workledger/cli/_commands/learn.py +9 -0
- workledger_cli-0.8.0/workledger/cli/_commands/list.py +56 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/workledger/cli/_commands/overview.py +2 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/workledger/explain/catalog.py +37 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/agent-config/SKILL.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/agent-config/scripts/show.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/ask-colleague/SKILL.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/ask-colleague/prompts/explore.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/ask-colleague/prompts/review.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/ask-colleague/prompts/write.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/ask-colleague/scripts/ask-colleague.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/challenge/SKILL.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/cicd/SKILL.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/cicd/scripts/workflow.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/communicate/SKILL.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/deviate/SKILL.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/recall/SKILL.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/recall/scripts/recall.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/remember/SKILL.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/remember/scripts/remember.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/run-tests/SKILL.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/scope/SKILL.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/sonarclaude/SKILL.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/spec-to-plan/SKILL.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/summarize-delivery/SKILL.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/think/SKILL.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/think/scripts/think.sh +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/version-bump/SKILL.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills/version-bump/scripts/bump.py +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.claude/skills.local.yaml.example +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.flake8 +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.github/workflows/publish.yml +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/.markdownlint-cli2.yaml +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/AGENTS.colleague.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/CLAUDE.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/LICENSE +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/culture.yaml +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/docs/skill-sources.md +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/sonar-project.properties +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/tests/__init__.py +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/tests/test_cli.py +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/tests/test_cli_introspection.py +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/workledger/__init__.py +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/workledger/__main__.py +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/workledger/cli/_commands/__init__.py +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/workledger/cli/_commands/cli.py +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/workledger/cli/_commands/doctor.py +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/workledger/cli/_commands/explain.py +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/workledger/cli/_commands/whoami.py +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/workledger/cli/_errors.py +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/workledger/cli/_output.py +0 -0
- {workledger_cli-0.7.0 → workledger_cli-0.8.0}/workledger/explain/__init__.py +0 -0
|
@@ -37,6 +37,18 @@ jobs:
|
|
|
37
37
|
env:
|
|
38
38
|
SONAR_HOST_URL: https://sonarcloud.io
|
|
39
39
|
|
|
40
|
+
# No unresolved SonarCloud issue on the PR. A code smell leaves the quality gate
|
|
41
|
+
# green, so this fails the job while the PR has any OPEN or CONFIRMED issue;
|
|
42
|
+
# ACCEPTED and FALSE_POSITIVE ones pass. Cited from culture-rules (#43) via
|
|
43
|
+
# culture-library.
|
|
44
|
+
- name: SonarCloud unresolved issues
|
|
45
|
+
if: env.SONAR_TOKEN != '' && github.event_name == 'pull_request'
|
|
46
|
+
env:
|
|
47
|
+
SONAR_HOST_URL: https://sonarcloud.io
|
|
48
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
49
|
+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
50
|
+
run: python3 scripts/sonar-pr-issues.py
|
|
51
|
+
|
|
40
52
|
lint:
|
|
41
53
|
runs-on: ubuntu-latest
|
|
42
54
|
permissions:
|
|
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
Format follows [Keep a Changelog](https://keepachangelog.com/). This project
|
|
6
6
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.8.0] - 2026-10-27
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `append` verb: append one JSON entry (agent, action, summary; optional refs, data) to an append-only JSONL ledger; `--stdin` and `--json` supported; id and UTC timestamp are generated.
|
|
13
|
+
- `list` verb: read entries with `--agent`, `--action`, `--since`, `--limit` filters; `--json` emits an array.
|
|
14
|
+
- Ledger location: `$WORKLEDGER_PATH`, default `.workledger/ledger.jsonl` under the git toplevel of the cwd (the cwd itself outside a repo). No edit/update/delete verb exists by design. `.workledger/` is git-ignored.
|
|
15
|
+
- Integrity: appends are complete writes (short writes are retried) and are refused with exit 2 if the file does not end in a newline. `append` rejects NaN/Infinity (also `1e999`), lone surrogates and control characters/newlines in `agent`/`action`/`summary` as user errors (exit 1) before touching disk; `--stdin` on an interactive terminal fails fast (exit 1).
|
|
16
|
+
- `list` streams the ledger (bounded memory with `--limit`), validates every line, and reports non-UTF-8 bytes, non-object lines, missing/malformed fields or timestamps, and duplicate ids as a corrupt ledger (exit 2, with the line number). `--since` is normalised once to UTC milliseconds; text output escapes control characters so an entry cannot forge extra rows.
|
|
17
|
+
- CI: a `SonarCloud unresolved issues` step fails a pull request while it has any OPEN or CONFIRMED SonarCloud issue (ACCEPTED and FALSE_POSITIVE pass), via `scripts/sonar-pr-issues.py` cited from culture-rules through culture-library.
|
|
18
|
+
|
|
8
19
|
## [0.7.0] - 2026-08-24
|
|
9
20
|
|
|
10
21
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: workledger-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Tracks work in a ledger so agents can prove what they did. An agent records its work as append-only ledger entries, hands the ledger down to subagents, and gets it back with their entries folded in, so the whole tree of work and its results stays traceable and checkable.
|
|
5
5
|
Project-URL: Homepage, https://github.com/agentculture/workledger-cli
|
|
6
6
|
Project-URL: Issues, https://github.com/agentculture/workledger-cli/issues
|
|
@@ -50,6 +50,8 @@ uv run teken cli doctor . --strict # the agent-first rubric gate CI runs
|
|
|
50
50
|
| `explain <path>` | Markdown docs for any noun/verb path. |
|
|
51
51
|
| `overview` | Read-only descriptive snapshot of the agent. |
|
|
52
52
|
| `doctor` | Check the agent-identity invariants (prompt-file-present, backend-consistency). |
|
|
53
|
+
| `append <json>` | Append one entry (`agent`, `action`, `summary`; optional `refs`, `data`) to the append-only ledger (`$WORKLEDGER_PATH`, default `.workledger/ledger.jsonl` under the git toplevel of the cwd (the cwd itself outside a repo)). `--stdin` reads JSON from stdin. |
|
|
54
|
+
| `list` | Read entries: `--agent`, `--action`, `--since`, `--limit`. |
|
|
53
55
|
| `cli overview` | Describe the CLI surface itself. |
|
|
54
56
|
|
|
55
57
|
Every command supports `--json`. Results go to stdout, errors/diagnostics to
|
|
@@ -33,6 +33,8 @@ uv run teken cli doctor . --strict # the agent-first rubric gate CI runs
|
|
|
33
33
|
| `explain <path>` | Markdown docs for any noun/verb path. |
|
|
34
34
|
| `overview` | Read-only descriptive snapshot of the agent. |
|
|
35
35
|
| `doctor` | Check the agent-identity invariants (prompt-file-present, backend-consistency). |
|
|
36
|
+
| `append <json>` | Append one entry (`agent`, `action`, `summary`; optional `refs`, `data`) to the append-only ledger (`$WORKLEDGER_PATH`, default `.workledger/ledger.jsonl` under the git toplevel of the cwd (the cwd itself outside a repo)). `--stdin` reads JSON from stdin. |
|
|
37
|
+
| `list` | Read entries: `--agent`, `--action`, `--since`, `--limit`. |
|
|
36
38
|
| `cli overview` | Describe the CLI surface itself. |
|
|
37
39
|
|
|
38
40
|
Every command supports `--json`. Results go to stdout, errors/diagnostics to
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "workledger-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "Tracks work in a ledger so agents can prove what they did. An agent records its work as append-only ledger entries, hands the ledger down to subagents, and gets it back with their entries folded in, so the whole tree of work and its results stays traceable and checkable."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Cited from agentculture/culture-rules@dcddf88 scripts/sonar-pr-issues.py (cite, don't import).
|
|
3
|
+
"""Fail a pull request that has unresolved SonarCloud issues.
|
|
4
|
+
|
|
5
|
+
SonarCloud's quality gate can pass with open issues on new code (a code smell does not
|
|
6
|
+
turn it red), so CI runs this right after the scan: it waits until SonarCloud's analysis
|
|
7
|
+
of the PR is for the PR's head commit, then lists the PR's issues whose status is OPEN or
|
|
8
|
+
CONFIRMED and exits 1 when there is any. ACCEPTED and FALSE_POSITIVE issues are resolved
|
|
9
|
+
by a person and pass; FIXED ones are gone.
|
|
10
|
+
|
|
11
|
+
Environment: ``SONAR_HOST_URL`` (the server; defaults to localhost so nothing outside is
|
|
12
|
+
called by accident), ``SONAR_PROJECT_KEY`` (else ``sonar.projectKey`` from
|
|
13
|
+
``sonar-project.properties``), ``PR_NUMBER``, ``HEAD_SHA`` and optionally ``SONAR_TOKEN``.
|
|
14
|
+
Exit codes: 0 no unresolved issue, 1 unresolved issues, 2 the analysis or the API could
|
|
15
|
+
not be read. Standard library only.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import base64
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
import time
|
|
25
|
+
import urllib.parse
|
|
26
|
+
import urllib.request
|
|
27
|
+
from collections.abc import Callable, Mapping
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
UNRESOLVED = ("OPEN", "CONFIRMED")
|
|
32
|
+
PAGE_SIZE = 100
|
|
33
|
+
WAIT_S = 300
|
|
34
|
+
POLL_S = 10
|
|
35
|
+
|
|
36
|
+
Fetch = Callable[[str, Mapping[str, str]], Mapping[str, Any]]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def project_key(env: Mapping[str, str], root: Path) -> str:
|
|
40
|
+
"""``SONAR_PROJECT_KEY``, else ``sonar.projectKey`` from ``sonar-project.properties``."""
|
|
41
|
+
if env.get("SONAR_PROJECT_KEY"):
|
|
42
|
+
return env["SONAR_PROJECT_KEY"]
|
|
43
|
+
for line in (root / "sonar-project.properties").read_text().splitlines():
|
|
44
|
+
name, _, value = line.partition("=")
|
|
45
|
+
if name.strip() == "sonar.projectKey" and value.strip():
|
|
46
|
+
return value.strip()
|
|
47
|
+
raise SystemExit("sonar-pr-issues: no SONAR_PROJECT_KEY and no sonar.projectKey")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def http_fetch(host: str, token: str | None) -> Fetch:
|
|
51
|
+
"""A GET of ``host + path`` with ``params``, as JSON (token as basic auth, if any)."""
|
|
52
|
+
|
|
53
|
+
def fetch(path: str, params: Mapping[str, str]) -> Mapping[str, Any]:
|
|
54
|
+
url = f"{host.rstrip('/')}{path}?{urllib.parse.urlencode(params)}"
|
|
55
|
+
req = urllib.request.Request(url) # noqa: S310 - the host is the configured server
|
|
56
|
+
if token:
|
|
57
|
+
basic = base64.b64encode(f"{token}:".encode()).decode()
|
|
58
|
+
req.add_header("Authorization", f"Basic {basic}")
|
|
59
|
+
with urllib.request.urlopen(req, timeout=30) as resp: # nosec B310
|
|
60
|
+
return json.load(resp)
|
|
61
|
+
|
|
62
|
+
return fetch
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def analysed_commit(fetch: Fetch, project: str, pr: str) -> str | None:
|
|
66
|
+
"""The commit SonarCloud last analysed for pull request ``pr``, or ``None``."""
|
|
67
|
+
data = fetch("/api/project_pull_requests/list", {"project": project})
|
|
68
|
+
for item in data.get("pullRequests") or ():
|
|
69
|
+
if str(item.get("key")) == pr:
|
|
70
|
+
commit = item.get("commit") or {}
|
|
71
|
+
sha = commit.get("sha")
|
|
72
|
+
return sha if isinstance(sha, str) else None
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def wait_for_analysis(
|
|
77
|
+
fetch: Fetch,
|
|
78
|
+
project: str,
|
|
79
|
+
pr: str,
|
|
80
|
+
head: str,
|
|
81
|
+
*,
|
|
82
|
+
wait_s: float = WAIT_S,
|
|
83
|
+
poll_s: float = POLL_S,
|
|
84
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
85
|
+
clock: Callable[[], float] = time.monotonic,
|
|
86
|
+
) -> bool:
|
|
87
|
+
"""True once the PR's analysis is for ``head``; False when ``wait_s`` runs out."""
|
|
88
|
+
deadline = clock() + wait_s
|
|
89
|
+
while True:
|
|
90
|
+
if analysed_commit(fetch, project, pr) == head:
|
|
91
|
+
return True
|
|
92
|
+
if clock() >= deadline:
|
|
93
|
+
return False
|
|
94
|
+
sleep(poll_s)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def unresolved_issues(fetch: Fetch, project: str, pr: str) -> list[Mapping[str, Any]]:
|
|
98
|
+
"""Every OPEN or CONFIRMED issue of pull request ``pr``, across pages."""
|
|
99
|
+
out: list[Mapping[str, Any]] = []
|
|
100
|
+
page = 1
|
|
101
|
+
while True:
|
|
102
|
+
data = fetch(
|
|
103
|
+
"/api/issues/search",
|
|
104
|
+
{
|
|
105
|
+
"componentKeys": project,
|
|
106
|
+
"pullRequest": pr,
|
|
107
|
+
"issueStatuses": ",".join(UNRESOLVED),
|
|
108
|
+
"ps": str(PAGE_SIZE),
|
|
109
|
+
"p": str(page),
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
issues = [i for i in data.get("issues") or () if isinstance(i, Mapping)]
|
|
113
|
+
out.extend(issues)
|
|
114
|
+
total = data.get("total")
|
|
115
|
+
if not issues or not isinstance(total, int) or len(out) >= total:
|
|
116
|
+
return out
|
|
117
|
+
page += 1
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def describe(issue: Mapping[str, Any], project: str) -> str:
|
|
121
|
+
"""One line: rule, file and line, severity, message."""
|
|
122
|
+
component = str(issue.get("component") or "").removeprefix(f"{project}:")
|
|
123
|
+
line = issue.get("line")
|
|
124
|
+
where = f"{component}:{line}" if line else component
|
|
125
|
+
return (
|
|
126
|
+
f"[{issue.get('rule')}] {where} ({issue.get('severity')}, "
|
|
127
|
+
f"{issue.get('issueStatus') or issue.get('status')}): {issue.get('message')}"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def main(
|
|
132
|
+
env: Mapping[str, str] | None = None,
|
|
133
|
+
fetch: Fetch | None = None,
|
|
134
|
+
root: Path | None = None,
|
|
135
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
136
|
+
) -> int:
|
|
137
|
+
env = os.environ if env is None else env
|
|
138
|
+
root = Path(__file__).resolve().parents[1] if root is None else root
|
|
139
|
+
pr, head = env.get("PR_NUMBER", ""), env.get("HEAD_SHA", "")
|
|
140
|
+
if not pr or not head:
|
|
141
|
+
print("sonar-pr-issues: PR_NUMBER and HEAD_SHA are required", file=sys.stderr)
|
|
142
|
+
return 2
|
|
143
|
+
project = project_key(env, root)
|
|
144
|
+
if fetch is None:
|
|
145
|
+
host = env.get("SONAR_HOST_URL") or "http://localhost:9000"
|
|
146
|
+
fetch = http_fetch(host, env.get("SONAR_TOKEN") or None)
|
|
147
|
+
try:
|
|
148
|
+
if not wait_for_analysis(fetch, project, pr, head, sleep=sleep):
|
|
149
|
+
print(
|
|
150
|
+
f"sonar-pr-issues: no SonarCloud analysis of PR #{pr} at {head[:12]}",
|
|
151
|
+
file=sys.stderr,
|
|
152
|
+
)
|
|
153
|
+
return 2
|
|
154
|
+
issues = unresolved_issues(fetch, project, pr)
|
|
155
|
+
except (OSError, ValueError) as exc: # URLError is an OSError; bad JSON a ValueError
|
|
156
|
+
print(f"sonar-pr-issues: SonarCloud could not be read: {exc}", file=sys.stderr)
|
|
157
|
+
return 2
|
|
158
|
+
if not issues:
|
|
159
|
+
print(f"SonarCloud: PR #{pr} has no unresolved issue (accepted ones pass)")
|
|
160
|
+
return 0
|
|
161
|
+
print(f"SonarCloud: PR #{pr} has {len(issues)} unresolved issue(s); fix or accept each:")
|
|
162
|
+
for issue in issues:
|
|
163
|
+
print(f" - {describe(issue, project)}")
|
|
164
|
+
return 1
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == "__main__":
|
|
168
|
+
sys.exit(main())
|