-
Notifications
You must be signed in to change notification settings - Fork 0
Align Codacy coverage upload and Sonar hotspot blocking #113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,9 @@ | |
|
|
||
|
|
||
| def _parse_args() -> argparse.Namespace: | ||
| parser = argparse.ArgumentParser(description="Assert SonarCloud has zero actionable open issues.") | ||
| parser = argparse.ArgumentParser( | ||
| description="Assert SonarCloud has zero actionable open issues and zero open security hotspots." | ||
| ) | ||
| parser.add_argument("--project-key", required=True, help="Sonar project key") | ||
| parser.add_argument("--token", default="", help="Sonar token (falls back to SONAR_TOKEN env)") | ||
| parser.add_argument("--branch", default="", help="Optional branch scope") | ||
|
|
@@ -76,7 +78,7 @@ def _query_sonar_status( | |
| project_key: str, | ||
| branch: str, | ||
| pull_request: str, | ||
| ) -> tuple[int, str]: | ||
| ) -> tuple[int, str, int]: | ||
| issues_query = { | ||
| "componentKeys": project_key, | ||
| "resolved": "false", | ||
|
|
@@ -101,19 +103,37 @@ def _query_sonar_status( | |
| gate_payload = _request_json(gate_url, auth) | ||
| project_status = (gate_payload.get("projectStatus") or {}) | ||
| quality_gate = str(project_status.get("status") or "UNKNOWN") | ||
| return open_issues, quality_gate | ||
|
|
||
| hotspot_query = { | ||
| "projectKey": project_key, | ||
| "status": "TO_REVIEW", | ||
| "ps": "1", | ||
| } | ||
| if branch: | ||
| hotspot_query["branch"] = branch | ||
| if pull_request: | ||
| hotspot_query["pullRequest"] = pull_request | ||
| hotspot_url = f"{api_base}/api/hotspots/search?{urllib.parse.urlencode(hotspot_query)}" | ||
| hotspot_payload = _request_json(hotspot_url, auth) | ||
| hotspot_paging = hotspot_payload.get("paging") or {} | ||
| open_hotspots = int(hotspot_paging.get("total") or 0) | ||
|
|
||
| return open_issues, quality_gate, open_hotspots | ||
|
|
||
|
|
||
| def evaluate_status( | ||
| *, | ||
| open_issues: int, | ||
| open_hotspots: int, | ||
| quality_gate: str, | ||
| require_quality_gate: bool, | ||
| ignore_open_issues: bool, | ||
| ) -> list[str]: | ||
| findings: list[str] = [] | ||
| if not ignore_open_issues and open_issues != 0: | ||
| findings.append(f"Sonar reports {open_issues} open issues (expected 0).") | ||
| if open_hotspots != 0: | ||
| findings.append(f"Sonar reports {open_hotspots} open security hotspots pending review (expected 0).") | ||
|
Comment on lines
133
to
+136
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Line 135 still fails when hotspots are present, so the CLI help on Lines 45-47 no longer matches the behavior. Please update the flag text or rename the flag so callers do not read “quality gate only” and then get a hotspot failure anyway. 📝 Suggested text update parser.add_argument(
"--ignore-open-issues",
action="store_true",
- help="Skip open-issue enforcement and evaluate quality gate only.",
+ help="Skip open-issue enforcement; quality-gate and hotspot checks still apply.",
)🤖 Prompt for AI Agents |
||
| if require_quality_gate and quality_gate != "OK": | ||
| findings.append(f"Sonar quality gate status is {quality_gate} (expected OK).") | ||
| return findings | ||
|
|
@@ -130,6 +150,7 @@ def _render_md(payload: dict) -> str: | |
| f"- Branch: `{payload.get('branch')}`", | ||
| f"- Pull request: `{payload.get('pull_request')}`", | ||
| f"- Open issues: `{payload.get('open_issues')}`", | ||
| f"- Open hotspots: `{payload.get('open_hotspots')}`", | ||
| f"- Quality gate: `{payload.get('quality_gate')}`", | ||
| f"- Timestamp (UTC): `{payload['timestamp_utc']}`", | ||
| "", | ||
|
|
@@ -170,6 +191,7 @@ def main() -> int: | |
| scope = "branch" | ||
| findings: list[str] = [] | ||
| open_issues: int | None = None | ||
| open_hotspots: int | None = None | ||
| quality_gate: str | None = None | ||
|
|
||
| if not token: | ||
|
|
@@ -178,7 +200,7 @@ def main() -> int: | |
| else: | ||
| auth = _auth_header(token) | ||
| try: | ||
| open_issues, quality_gate = _query_sonar_status( | ||
| open_issues, quality_gate, open_hotspots = _query_sonar_status( | ||
| api_base=api_base, | ||
| auth=auth, | ||
| project_key=args.project_key, | ||
|
|
@@ -187,11 +209,11 @@ def main() -> int: | |
| ) | ||
| quality_gate = quality_gate or "UNKNOWN" | ||
|
|
||
| if args.pull_request and open_issues != 0 and args.wait_seconds > 0: | ||
| if args.pull_request and (open_issues != 0 or open_hotspots != 0) and args.wait_seconds > 0: | ||
| deadline = time.time() + max(0, args.wait_seconds) | ||
| while open_issues != 0 and time.time() < deadline: | ||
| while (open_issues != 0 or open_hotspots != 0) and time.time() < deadline: | ||
| time.sleep(10) | ||
| open_issues, quality_gate = _query_sonar_status( | ||
| open_issues, quality_gate, open_hotspots = _query_sonar_status( | ||
| api_base=api_base, | ||
| auth=auth, | ||
| project_key=args.project_key, | ||
|
|
@@ -203,6 +225,7 @@ def main() -> int: | |
| findings.extend( | ||
| evaluate_status( | ||
| open_issues=open_issues, | ||
| open_hotspots=open_hotspots, | ||
| quality_gate=quality_gate, | ||
| require_quality_gate=args.require_quality_gate, | ||
| ignore_open_issues=args.ignore_open_issues, | ||
|
|
@@ -221,6 +244,7 @@ def main() -> int: | |
| "branch": args.branch or None, | ||
| "pull_request": args.pull_request or None, | ||
| "open_issues": open_issues, | ||
| "open_hotspots": open_hotspots, | ||
| "quality_gate": quality_gate, | ||
| "ignore_open_issues": bool(args.ignore_open_issues), | ||
| "timestamp_utc": datetime.now(timezone.utc).isoformat(), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Codacy coverage reporter authentication CODACY_PROJECT_TOKEN single repository💡 Result:
For a single repository, Codacy Coverage Reporter authenticates using a Project (repository) API token, exposed as
CODACY_PROJECT_TOKEN(or passed as--project-token). You don’t needCODACY_API_TOKENunless you’re automating uploads for multiple repositories from the same workflow/account. [1][3]Single-repo (recommended)
CODACY_PROJECT_TOKEN=<token>[1]codacy-coverage-reporter report --project-token <token> ...[3]GitHub Actions example (single repo)
CODACY_PROJECT_TOKEN, then pass it asproject-token: ${{ secrets.CODACY_PROJECT_TOKEN }}. [2]When to use
CODACY_API_TOKENinsteadCODACY_API_TOKEN) plusCODACY_ORGANIZATION_PROVIDER,CODACY_USERNAME, andCODACY_PROJECT_NAME. [1][2]Sources
codacy/codacy-coverage-reporter-actionREADME (project-token vs api-token) [2]--project-token,--api-token, etc.) [3]Use a repository-scoped Codacy token.
This workflow uploads coverage for a single repository but uses
CODACY_API_TOKEN, which Codacy documents for multi-repository automation. For single-repo uploads,CODACY_PROJECT_TOKENis the recommended approach and avoids granting unnecessary account-level access in CI.Narrow the secret scope
- name: Upload coverage to Codacy if: ${{ always() }} env: - CODACY_API_TOKEN: ${{ secrets.CODACY_API_TOKEN }} - CODACY_ORGANIZATION_PROVIDER: gh - CODACY_USERNAME: Prekzursil - CODACY_PROJECT_NAME: ${{ github.event.repository.name }} + CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} run: | - if [ -z "${CODACY_API_TOKEN}" ]; then - echo "Missing CODACY_API_TOKEN" >&2 + if [ -z "${CODACY_PROJECT_TOKEN}" ]; then + echo "Missing CODACY_PROJECT_TOKEN" >&2 exit 1 fi📝 Committable suggestion
🤖 Prompt for AI Agents