diff --git a/.github/scripts/compose_telegram_message.py b/.github/scripts/compose_telegram_message.py
new file mode 100644
index 0000000..48ee761
--- /dev/null
+++ b/.github/scripts/compose_telegram_message.py
@@ -0,0 +1,93 @@
+import html
+import os
+
+FAILURE_STATES = {"failure", "cancelled", "timed_out", "action_required"}
+SUCCESS_STATES = {"success", "skipped", "neutral"}
+
+JOB_RESULT_FIELDS = (
+ ("backend", "BACKEND_RESULT"),
+ ("sonar-backend", "SONAR_BACKEND_RESULT"),
+)
+
+SONAR_SKIP_FLAGS = {
+ "SONAR_BACKEND_RESULT": "SONAR_BACKEND_SKIPPED",
+}
+
+
+def normalize(value: str) -> str:
+ return (value or "unknown").lower()
+
+
+def status_icon(status: str) -> str:
+ if status == "success":
+ return "✅"
+ if status in FAILURE_STATES:
+ return "❌"
+ if status in {"skipped", "neutral"}:
+ return "⏭️"
+ return "❓"
+
+
+def get_actual_status(env_name: str, result: str) -> str:
+ skip_flag = SONAR_SKIP_FLAGS.get(env_name)
+ if skip_flag and normalize(os.environ.get(skip_flag, "")) == "true":
+ return "skipped"
+ return result
+
+
+def overall_status() -> str:
+ statuses = []
+ for _, env_name in JOB_RESULT_FIELDS:
+ result = normalize(os.environ.get(env_name, "unknown"))
+ actual = get_actual_status(env_name, result)
+ statuses.append(actual)
+
+ if any(status in FAILURE_STATES for status in statuses):
+ return "failure"
+ if all(status in SUCCESS_STATES for status in statuses):
+ return "success"
+ return "unknown"
+
+
+def esc(value: str) -> str:
+ return html.escape(str(value), quote=True)
+
+
+def compose_message() -> str:
+ current_overall = overall_status()
+ lines = [
+ "jwt-demo-reactive CI finished",
+ "",
+ f"{status_icon(current_overall)} Status: {esc(current_overall)}",
+ f"Branch: {esc(os.environ.get('GITHUB_REF_NAME', ''))}",
+ f"Commit: {esc(os.environ.get('GITHUB_SHA', ''))}",
+ f"Actor: {esc(os.environ.get('GITHUB_ACTOR', ''))}",
+ f"Workflow: {esc(os.environ.get('GITHUB_WORKFLOW', ''))}",
+ "",
+ "Job results",
+ ]
+
+ for job_name, env_name in JOB_RESULT_FIELDS:
+ result = normalize(os.environ.get(env_name, "unknown"))
+ actual = get_actual_status(env_name, result)
+ lines.append(f"- {status_icon(actual)} {esc(job_name)}: {esc(actual)}")
+
+ lines.append("")
+ lines.append(
+ f"Link: {esc(os.environ.get('GITHUB_SERVER_URL', ''))}/{esc(os.environ.get('GITHUB_REPOSITORY', ''))}/actions/runs/{esc(os.environ.get('GITHUB_RUN_ID', ''))}"
+ )
+
+ return "\n".join(lines)
+
+
+def main() -> None:
+ message = compose_message()
+ output_path = os.environ["GITHUB_OUTPUT"]
+ with open(output_path, "a", encoding="utf-8") as output_file:
+ output_file.write("message< dict[str, str]:
+ data: dict[str, str] = {}
+ for raw_line in path.read_text(encoding="utf-8").splitlines():
+ line = raw_line.strip()
+ if not line or line.startswith("#") or "=" not in line:
+ continue
+ key, value = line.split("=", 1)
+ data[key.strip()] = value.strip()
+ return data
+
+
+def api_get_json(url: str, token: str) -> dict:
+ request = urllib.request.Request(url)
+ request.add_header("Authorization", f"Basic {build_basic_auth(token)}")
+ try:
+ with urllib.request.urlopen(request, timeout=30) as response:
+ return json.loads(response.read().decode("utf-8"))
+ except urllib.error.HTTPError as error:
+ response_body = error.read().decode("utf-8", errors="replace")
+ raise RuntimeError(
+ f"Sonar API request failed: {url} returned HTTP {error.code}. Response: {response_body}"
+ ) from error
+ except urllib.error.URLError as error:
+ raise RuntimeError(f"Sonar API request failed: {url}. Reason: {error.reason}") from error
+
+
+def build_basic_auth(token: str) -> str:
+ import base64
+
+ raw = f"{token}:".encode("utf-8")
+ return base64.b64encode(raw).decode("ascii")
+
+
+def wait_for_ce_task(ce_task_url: str, token: str, timeout_seconds: int, poll_seconds: int) -> dict:
+ started = time.monotonic()
+ while True:
+ payload = api_get_json(ce_task_url, token)
+ task = payload.get("task", {})
+ status = task.get("status")
+
+ if status in {"SUCCESS", "FAILED", "CANCELED"}:
+ return task
+
+ if (time.monotonic() - started) > timeout_seconds:
+ raise TimeoutError(f"Timed out waiting for Sonar CE task at {ce_task_url}")
+
+ time.sleep(poll_seconds)
+
+
+def build_measures_url(host_url: str, project_key: str, pull_request: str | None, branch: str | None) -> str:
+ metric_keys = ",".join(
+ [
+ "coverage",
+ "new_coverage",
+ "bugs",
+ "new_bugs",
+ "vulnerabilities",
+ "new_vulnerabilities",
+ "code_smells",
+ "new_code_smells",
+ "duplicated_lines_density",
+ "new_duplicated_lines_density",
+ ]
+ )
+ query = {
+ "component": project_key,
+ "metricKeys": metric_keys,
+ }
+ if pull_request:
+ query["pullRequest"] = pull_request
+ elif branch:
+ query["branch"] = branch
+ return f"{host_url}/api/measures/component?{urllib.parse.urlencode(query)}"
+
+
+def detect_analysis_scope() -> tuple[str | None, str | None]:
+ event_path = os.getenv("GITHUB_EVENT_PATH", "")
+ if event_path:
+ try:
+ event_payload = json.loads(Path(event_path).read_text(encoding="utf-8"))
+ except (OSError, json.JSONDecodeError):
+ event_payload = {}
+ pull_request_number = event_payload.get("pull_request", {}).get("number")
+ if pull_request_number is not None:
+ return str(pull_request_number), None
+
+ branch = (os.getenv("GITHUB_HEAD_REF") or os.getenv("GITHUB_REF_NAME") or "").strip()
+ return None, branch or None
+
+
+def to_measure_map(payload: dict) -> dict[str, str]:
+ component = payload.get("component", {})
+ result: dict[str, str] = {}
+ for measure in component.get("measures", []):
+ result[measure.get("metric")] = measure.get("value", "-")
+ return result
+
+
+def fetch_measures(host_url: str, project_key: str, token: str) -> dict[str, str]:
+ pull_request, branch = detect_analysis_scope()
+ scoped_url = build_measures_url(host_url, project_key, pull_request, branch)
+
+ try:
+ return to_measure_map(api_get_json(scoped_url, token))
+ except RuntimeError as scoped_error:
+ base_url = build_measures_url(host_url, project_key, None, None)
+ if base_url != scoped_url:
+ try:
+ print(
+ "Scoped measures query failed; retrying without branch/pullRequest context",
+ file=sys.stderr,
+ )
+ return to_measure_map(api_get_json(base_url, token))
+ except RuntimeError as base_error:
+ print(f"Unable to load Sonar measures: {base_error}", file=sys.stderr)
+ return {}
+
+ print(f"Unable to load Sonar measures: {scoped_error}", file=sys.stderr)
+ return {}
+
+
+def append_summary(text: str) -> None:
+ summary_file = os.getenv("GITHUB_STEP_SUMMARY")
+ if not summary_file:
+ return
+ with open(summary_file, "a", encoding="utf-8") as handle:
+ handle.write(text)
+
+
+def is_missing_new_code_metrics_only(gate_status: str, conditions: list[dict], measures: dict[str, str]) -> bool:
+ if gate_status != "ERROR":
+ return False
+
+ if measures.get("new_coverage", "-") not in {"-", "NO_VALUE", ""}:
+ return False
+
+ if not conditions:
+ return False
+
+ has_non_ok_condition = False
+ for condition in conditions:
+ status = (condition.get("status") or "").upper()
+ if status in {"", "OK"}:
+ continue
+
+ has_non_ok_condition = True
+ metric_key = condition.get("metricKey", "")
+ actual_value = condition.get("actualValue")
+ normalized_actual = "-" if actual_value in (None, "") else str(actual_value)
+
+ if not metric_key.startswith("new_"):
+ return False
+ if normalized_actual not in {"-", "NO_VALUE", "None"}:
+ return False
+
+ return has_non_ok_condition
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--project-key", required=True)
+ parser.add_argument("--component-name", required=True)
+ parser.add_argument("--report-task-file", required=True)
+ parser.add_argument("--timeout-seconds", type=int, default=300)
+ parser.add_argument("--poll-seconds", type=int, default=5)
+ parser.add_argument(
+ "--allow-missing-new-code-metrics",
+ action="store_true",
+ help="Do not fail when Sonar returns ERROR only because new_* metrics are unavailable",
+ )
+ args = parser.parse_args()
+
+ token = os.getenv("SONAR_TOKEN", "")
+ if not token:
+ print("SONAR_TOKEN is missing", file=sys.stderr)
+ return 2
+
+ configured_host_url = (os.getenv("SONAR_HOST_URL") or "").strip()
+ report_task_path = Path(args.report_task_file)
+ if not report_task_path.exists():
+ print(f"report-task.txt not found: {report_task_path}", file=sys.stderr)
+ return 2
+
+ report = parse_kv_file(report_task_path)
+ report_server_url = (report.get("serverUrl") or "").strip()
+ host_url = (configured_host_url or report_server_url or "https://sonarcloud.io").rstrip("/")
+ ce_task_url = report.get("ceTaskUrl")
+ if not ce_task_url:
+ print("ceTaskUrl is missing in report-task.txt", file=sys.stderr)
+ return 2
+
+ try:
+ print(f"Waiting for Sonar CE task: {ce_task_url}")
+ task = wait_for_ce_task(ce_task_url, token, args.timeout_seconds, args.poll_seconds)
+ task_status = task.get("status", "UNKNOWN")
+ analysis_id = task.get("analysisId")
+
+ if task_status != "SUCCESS" or not analysis_id:
+ print(f"Sonar CE task status is {task_status}; analysisId={analysis_id}", file=sys.stderr)
+ return 1
+
+ qg_url = f"{host_url}/api/qualitygates/project_status?analysisId={urllib.parse.quote(analysis_id)}"
+ qg_payload = api_get_json(qg_url, token)
+ project_status = qg_payload.get("projectStatus", {})
+ gate_status = project_status.get("status", "NONE")
+ conditions = project_status.get("conditions", [])
+
+ measures = fetch_measures(host_url, args.project_key, token)
+ except (RuntimeError, TimeoutError) as error:
+ print(str(error), file=sys.stderr)
+ return 1
+
+ print(f"Quality Gate ({args.component_name}): {gate_status}")
+ print("Measures:")
+ for metric_key in [
+ "coverage",
+ "new_coverage",
+ "bugs",
+ "new_bugs",
+ "vulnerabilities",
+ "new_vulnerabilities",
+ "code_smells",
+ "new_code_smells",
+ "duplicated_lines_density",
+ "new_duplicated_lines_density",
+ ]:
+ print(f" {metric_key}: {measures.get(metric_key, '-')}")
+
+ summary_lines = [
+ f"### SonarQube - {args.component_name}",
+ "",
+ f"- Quality Gate: **{gate_status}**",
+ f"- Project key: `{args.project_key}`",
+ "",
+ "| Metric | Value |",
+ "|---|---:|",
+ ]
+ for metric_key in [
+ "coverage",
+ "new_coverage",
+ "bugs",
+ "new_bugs",
+ "vulnerabilities",
+ "new_vulnerabilities",
+ "code_smells",
+ "new_code_smells",
+ "duplicated_lines_density",
+ "new_duplicated_lines_density",
+ ]:
+ summary_lines.append(f"| `{metric_key}` | {measures.get(metric_key, '-')} |")
+
+ summary_lines.extend(["", "| Condition metric | Status | Actual | Threshold |", "|---|---|---:|---:|"])
+ if conditions:
+ for condition in conditions:
+ summary_lines.append(
+ "| `{}` | {} | {} | {} |".format(
+ condition.get("metricKey", "-"),
+ condition.get("status", "-"),
+ condition.get("actualValue", "-"),
+ condition.get("errorThreshold", "-"),
+ )
+ )
+ else:
+ summary_lines.append("| `-` | - | - | - |")
+
+ summary_lines.append("\n")
+ append_summary("\n".join(summary_lines))
+
+ if gate_status != "OK":
+ if args.allow_missing_new_code_metrics and is_missing_new_code_metrics_only(gate_status, conditions, measures):
+ print(
+ "Quality Gate returned ERROR due to unavailable new-code metrics; treated as neutral for this run"
+ )
+ return 0
+ print("Quality Gate failed", file=sys.stderr)
+ return 1
+
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b3f92c1..7bc43d1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,8 +1,5 @@
name: CI
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
on:
push:
branches: ["**"]
@@ -10,156 +7,203 @@ on:
permissions:
contents: read
- checks: write
- actions: write
- pull-requests: write
+ actions: read
jobs:
- build:
+ backend:
+ name: Backend (Maven)
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ actions: write
+
steps:
- - name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- fetch-depth: 0
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up JDK 25
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: temurin
- java-version: "25"
+ java-version: 25
cache: maven
- - name: Cache SonarQube packages
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- with:
- path: ~/.sonar/cache
- key: ${{ runner.os }}-sonar
- restore-keys: ${{ runner.os }}-sonar
-
- - name: Build and verify
+ - name: Run backend tests
run: mvn -B -ntp verify
- - name: Upload Unit Test Reports
+ - name: Upload backend JaCoCo report
if: always()
- uses: actions/upload-artifact@v7
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
- name: unit-test-reports
- path: target/surefire-reports/TEST-*.xml
+ name: backend-jacoco-report
+ path: target/site/jacoco-merged/jacoco.xml
if-no-files-found: warn
retention-days: 7
- - name: Upload Integration Test Reports
- if: always()
- uses: actions/upload-artifact@v7
+ sonar-backend:
+ name: SonarQube - backend
+ runs-on: ubuntu-latest
+ needs: [backend]
+ if: >
+ ((github.event_name == 'push' && github.ref == 'refs/heads/main') ||
+ (github.event_name == 'pull_request' && github.actor != 'dependabot[bot]'))
+ outputs:
+ sonar_skipped: ${{ steps.sonar-check.outputs.sonar_skipped }}
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
+ SONAR_ORGANIZATION: ${{ vars.SONAR_ORGANIZATION }}
+ SONAR_PROJECT_KEY: ${{ vars.SONAR_PROJECT_KEY }}
+
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
- name: integration-test-reports
- path: target/failsafe-reports/TEST-*.xml
- if-no-files-found: warn
- retention-days: 7
+ fetch-depth: 0
- - name: Test Reports Summary (Actions UI)
- if: always()
- shell: bash
+ - name: Check Sonar configuration
+ id: sonar-check
run: |
- unit_count=$(find target/surefire-reports -maxdepth 1 -name 'TEST-*.xml' 2>/dev/null | wc -l)
- it_count=$(find target/failsafe-reports -maxdepth 1 -name 'TEST-*.xml' 2>/dev/null | wc -l)
- {
- echo "## Test Reports"
- echo "- Unit JUnit XML files: ${unit_count} (artifact: unit-test-reports)"
- echo "- Integration JUnit XML files: ${it_count} (artifact: integration-test-reports)"
- } >> "$GITHUB_STEP_SUMMARY"
-
- - name: Unit Test Summary
- if: always()
- id: unit
- uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0
+ if [ -z "$SONAR_TOKEN" ]; then
+ echo "sonar_skipped=true" >> "$GITHUB_OUTPUT"
+ echo "SONAR_TOKEN not set; skipping backend analysis"
+ exit 0
+ fi
+ host_url="${SONAR_HOST_URL:-https://sonarcloud.io}"
+ host_url="${host_url%/}"
+ if [ "$host_url" = "https://sonarcloud.io" ] && [ -z "$SONAR_ORGANIZATION" ]; then
+ echo "sonar_skipped=true" >> "$GITHUB_OUTPUT"
+ echo "SONAR_ORGANIZATION not set for SonarCloud; skipping backend analysis"
+ exit 0
+ fi
+ echo "sonar_skipped=false" >> "$GITHUB_OUTPUT"
+ echo "sonar_enabled=true" >> "$GITHUB_OUTPUT"
+
+ - name: Set up JDK 25
+ if: steps.sonar-check.outputs.sonar_enabled == 'true'
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
- token: ${{ secrets.GITHUB_TOKEN }}
- name: Unit Tests (JUnit)
- path: target/surefire-reports/TEST-*.xml
- reporter: java-junit
- fail-on-error: false
- fail-on-empty: false
- use-actions-summary: true
-
- - name: Integration Test Summary
- if: always()
- id: integration
- uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0
+ distribution: temurin
+ java-version: 25
+ cache: maven
+
+ - name: Download backend JaCoCo report
+ if: steps.sonar-check.outputs.sonar_enabled == 'true'
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
- token: ${{ secrets.GITHUB_TOKEN }}
- name: Integration Tests (JUnit)
- path: target/failsafe-reports/TEST-*.xml
- reporter: java-junit
- fail-on-error: false
- fail-on-empty: false
- use-actions-summary: true
-
- # -----------------------------
- # Telegram notification
- # -----------------------------
+ name: backend-jacoco-report
+ path: coverage
+
+ - name: Run SonarQube analysis (backend)
+ if: steps.sonar-check.outputs.sonar_enabled == 'true'
+ shell: bash
+ env:
+ REPO_NAME: ${{ github.event.repository.name }}
+ EVENT_NAME: ${{ github.event_name }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ PR_HEAD_REF: ${{ github.head_ref }}
+ PR_BASE_REF: ${{ github.base_ref }}
+ run: |
+ host_url="${SONAR_HOST_URL:-https://sonarcloud.io}"
+ sonar_project_key="${SONAR_PROJECT_KEY:-$REPO_NAME}"
+ report_path=""
+ found_report=""
+ if [ -d "coverage" ]; then
+ found_report=$(find coverage -type f -name "jacoco.xml" | head -1 || true)
+ fi
+ if [ -n "${found_report}" ]; then
+ report_path="$(pwd)/${found_report}"
+ fi
+ cmd=(
+ mvn -B -ntp
+ -Djacoco.skip=true
+ -DskipTests
+ verify
+ sonar:sonar
+ "-Dsonar.host.url=${host_url}"
+ "-Dsonar.projectKey=${sonar_project_key}"
+ "-Dsonar.token=${SONAR_TOKEN}"
+ )
+
+ if [ -n "${report_path}" ]; then
+ cmd+=("-Dsonar.coverage.jacoco.xmlReportPaths=${report_path}")
+ else
+ echo "JaCoCo XML report not found; Sonar will run without coverage input"
+ fi
+
+ if [ -n "${SONAR_ORGANIZATION}" ]; then
+ cmd+=("-Dsonar.organization=${SONAR_ORGANIZATION}")
+ fi
+
+ if [ "${EVENT_NAME}" = "pull_request" ] && [ -n "${PR_NUMBER}" ] && [ -n "${PR_HEAD_REF}" ] && [ -n "${PR_BASE_REF}" ]; then
+ cmd+=("-Dsonar.pullrequest.key=${PR_NUMBER}")
+ cmd+=("-Dsonar.pullrequest.branch=${PR_HEAD_REF}")
+ cmd+=("-Dsonar.pullrequest.base=${PR_BASE_REF}")
+ fi
+
+ "${cmd[@]}"
+
+ - name: Locate backend Sonar report-task
+ if: steps.sonar-check.outputs.sonar_enabled == 'true'
+ id: report-task
+ shell: bash
+ run: |
+ report_task=$(find . -type f \( -path "*/target/sonar/report-task.txt" -o -path "*/.scannerwork/report-task.txt" \) | head -1)
+ if [ -z "$report_task" ]; then
+ echo "Unable to find Sonar report-task.txt" >&2
+ exit 1
+ fi
+ echo "path=${report_task}" >> "$GITHUB_OUTPUT"
+
+ - name: Wait for Quality Gate and publish backend Sonar summary
+ if: steps.sonar-check.outputs.sonar_enabled == 'true'
+ shell: bash
+ env:
+ REPO_NAME: ${{ github.event.repository.name }}
+ run: |
+ sonar_project_key="${SONAR_PROJECT_KEY:-$REPO_NAME}"
+ extra_args=()
+ if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
+ extra_args+=("--allow-missing-new-code-metrics")
+ fi
+
+ python3 .github/scripts/sonar_quality_gate_report.py \
+ --component-name "backend" \
+ --project-key "${sonar_project_key}" \
+ --report-task-file "${{ steps.report-task.outputs.path }}" \
+ --timeout-seconds 900 \
+ "${extra_args[@]}"
+
+ notify-telegram:
+ name: Notify Telegram
+ runs-on: ubuntu-latest
+ needs: [backend, sonar-backend]
+ if: ${{ always() }}
+ permissions:
+ contents: read
+ env:
+ TELEGRAM_TO: ${{ secrets.TELEGRAM_TO }}
+ TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
+
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Compose Telegram message
+ id: compose
+ run: python3 .github/scripts/compose_telegram_message.py
+ env:
+ BACKEND_RESULT: ${{ needs.backend.result }}
+ SONAR_BACKEND_RESULT: ${{ needs.sonar-backend.result }}
+ SONAR_BACKEND_SKIPPED: ${{ needs.sonar-backend.outputs.sonar_skipped }}
+
- name: Notify Telegram
- if: always()
- uses: appleboy/telegram-action@master
+ if: ${{ env.TELEGRAM_TO != '' && env.TELEGRAM_TOKEN != '' }}
+ uses: appleboy/telegram-action@221e6b684967abe813051ee4a37dd61770a83ad3 # v1.0.1
with:
to: ${{ secrets.TELEGRAM_TO }}
token: ${{ secrets.TELEGRAM_TOKEN }}
- message: |
- 🚀 jwt-demo-reactive CI завершён
-
- *Статус:* ${{ job.status }}
- *Бранч:* ${{ github.ref_name }}
- *Коммит:* ${{ github.sha }}
- *Автор:* ${{ github.actor }}
-
- 🧪 *Unit Tests*
- - Total: ${{ steps.unit.outputs.total }}
- - Passed: ${{ steps.unit.outputs.passed }}
- - Failed: ${{ steps.unit.outputs.failed }}
- - Skipped: ${{ steps.unit.outputs.skipped }}
-
- 🔧 *Integration Tests*
- - Total: ${{ steps.integration.outputs.total }}
- - Passed: ${{ steps.integration.outputs.passed }}
- - Failed: ${{ steps.integration.outputs.failed }}
- - Skipped: ${{ steps.integration.outputs.skipped }}
- format: markdown
-
- # -----------------------------
- # PR Comment with test results
- # -----------------------------
- - name: Post Test Summary as PR Comment
- if: github.event_name == 'pull_request'
- uses: marocchino/sticky-pull-request-comment@v3
- with:
- header: "Test Results"
- message: |
- ## 🧪 Test Results
-
- ### Unit Tests
- - Total: ${{ steps.unit.outputs.total }}
- - Passed: ${{ steps.unit.outputs.passed }}
- - Failed: ${{ steps.unit.outputs.failed }}
- - Skipped: ${{ steps.unit.outputs.skipped }}
-
- ### Integration Tests
- - Total: ${{ steps.integration.outputs.total }}
- - Passed: ${{ steps.integration.outputs.passed }}
- - Failed: ${{ steps.integration.outputs.failed }}
- - Skipped: ${{ steps.integration.outputs.skipped }}
-
- - name: SonarCloud analysis
- if: >
- (github.event_name == 'push' && github.ref == 'refs/heads/main') ||
- (github.event_name == 'pull_request' && github.actor != 'dependabot[bot]')
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- SONAR_PROJECT_KEY: ${{ vars.SONAR_PROJECT_KEY }}
- SONAR_ORGANIZATION: ${{ vars.SONAR_ORGANIZATION }}
- run: >-
- mvn -B -ntp sonar:sonar
- -Dsonar.host.url=https://sonarcloud.io
- -Dsonar.projectKey=${SONAR_PROJECT_KEY}
- -Dsonar.organization=${SONAR_ORGANIZATION}
- -Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco-merged/jacoco.xml
\ No newline at end of file
+ message: ${{ steps.compose.outputs.message }}
+ format: html
+
+ - name: Telegram skipped (missing secrets)
+ if: ${{ env.TELEGRAM_TO == '' || env.TELEGRAM_TOKEN == '' }}
+ run: echo "TELEGRAM_TO and/or TELEGRAM_TOKEN are not set. Skipping Telegram notification."
\ No newline at end of file