diff --git a/mergify_cli/ci/detector.py b/mergify_cli/ci/detector.py index 06f12ba6..fb6d5d33 100644 --- a/mergify_cli/ci/detector.py +++ b/mergify_cli/ci/detector.py @@ -73,6 +73,25 @@ async def get_head_sha() -> str | None: return None +def get_cicd_pipeline_run_id() -> int | None: + if get_ci_provider() == "github_actions" and "GITHUB_RUN_ID" in os.environ: + return int(os.environ["GITHUB_RUN_ID"]) + + if get_ci_provider() == "circleci" and "CIRCLE_WORKFLOW_ID" in os.environ: + return int(os.environ["CIRCLE_WORKFLOW_ID"]) + + return None + + +def get_cicd_pipeline_run_attempt() -> int | None: + if get_ci_provider() == "github_actions" and "GITHUB_RUN_ATTEMPT" in os.environ: + return int(os.environ["GITHUB_RUN_ATTEMPT"]) + if get_ci_provider() == "circleci" and "CIRCLE_BUILD_NUM" in os.environ: + return int(os.environ["CIRCLE_BUILD_NUM"]) + + return None + + def get_github_repository() -> str | None: if get_ci_provider() == "github_actions": return os.getenv("GITHUB_REPOSITORY") diff --git a/mergify_cli/ci/junit.py b/mergify_cli/ci/junit.py index f3c0b61f..5de43874 100644 --- a/mergify_cli/ci/junit.py +++ b/mergify_cli/ci/junit.py @@ -34,7 +34,6 @@ class InvalidJunitXMLError(Exception): async def junit_to_spans( - trace_id: int, xml_content: bytes, test_language: str | None = None, test_framework: str | None = None, @@ -69,11 +68,17 @@ async def junit_to_spans( if test_language is not None: common_attributes["test.language"] = test_language - resource_attributes = {} + resource_attributes: dict[str, typing.Any] = {} if (job_name := detector.get_job_name()) is not None: resource_attributes[cicd_attributes.CICD_PIPELINE_NAME] = job_name + if (run_id := detector.get_cicd_pipeline_run_id()) is not None: + resource_attributes[cicd_attributes.CICD_PIPELINE_RUN_ID] = run_id + + if (run_attempt := detector.get_cicd_pipeline_run_attempt()) is not None: + resource_attributes["cicd.pipeline.run.attempt"] = run_attempt + if (head_revision := (await detector.get_head_sha())) is not None: resource_attributes[vcs_attributes.VCS_REF_HEAD_REVISION] = head_revision @@ -82,6 +87,8 @@ async def junit_to_spans( resource = resources.Resource.create(resource_attributes) + trace_id = ID_GENERATOR.generate_trace_id() + for testsuite in testsuites: min_start_time = now suite_name = testsuite.get("name", "unnamed testsuite") diff --git a/mergify_cli/ci/upload.py b/mergify_cli/ci/upload.py index 17f0a9de..2c93e73c 100644 --- a/mergify_cli/ci/upload.py +++ b/mergify_cli/ci/upload.py @@ -57,9 +57,9 @@ def upload_spans( def connect_traces(spans: list[ReadableSpan]) -> None: if detector.get_ci_provider() == "github_actions" and spans: - trace_id = spans[0].context.trace_id + root_span_id = spans[0].context.span_id console.print( - f"::notice title=Mergify CI::MERGIFY_TRACE_ID={trace_id}", + f"::notice title=Mergify CI::MERGIFY_TEST_ROOT_SPAN_ID={root_span_id}", soft_wrap=True, ) @@ -74,13 +74,10 @@ async def upload( # noqa: PLR0913, PLR0917 ) -> None: spans = [] - trace_id = junit.ID_GENERATOR.generate_trace_id() - for filename in files: try: spans.extend( await junit.junit_to_spans( - trace_id, pathlib.Path(filename).read_bytes(), test_language=test_language, test_framework=test_framework, diff --git a/mergify_cli/tests/ci/test_junit.py b/mergify_cli/tests/ci/test_junit.py index 2ab0c130..de20c210 100644 --- a/mergify_cli/tests/ci/test_junit.py +++ b/mergify_cli/tests/ci/test_junit.py @@ -11,6 +11,8 @@ @mock.patch.object(detector, "get_ci_provider", return_value="github_actions") @mock.patch.object(detector, "get_job_name", return_value="JOB") +@mock.patch.object(detector, "get_cicd_pipeline_run_id", return_value=123) +@mock.patch.object(detector, "get_cicd_pipeline_run_attempt", return_value=1) @mock.patch.object( detector, "get_head_sha", @@ -20,10 +22,11 @@ async def test_parse( _get_ci_provider: mock.Mock, _get_job_name: mock.Mock, _get_head_sha: mock.Mock, + _get_cicd_pipeline_run_id: mock.Mock, + _get_cicd_pipeline_run_attempt: mock.Mock, ) -> None: filename = pathlib.Path(__file__).parent / "junit_example.xml" spans = await junit.junit_to_spans( - 123, filename.read_bytes(), "python", "unittest", @@ -32,6 +35,17 @@ async def test_parse( trace_id = "0x" + opentelemetry.trace.span.format_trace_id( spans[1].context.trace_id, ) + resource_attributes = { + "cicd.pipeline.name": "JOB", + "cicd.pipeline.run.id": 123, + "cicd.pipeline.run.attempt": 1, + "cicd.provider.name": "github_actions", + "vcs.ref.head.revision": "3af96aa24f1d32fcfbb7067793cacc6dc0c6b199", + "service.name": "unknown_service", + "telemetry.sdk.language": "python", + "telemetry.sdk.name": "opentelemetry", + "telemetry.sdk.version": anys.ANY_STR, + } assert dictified_spans == [ { "attributes": { @@ -52,15 +66,7 @@ async def test_parse( "name": "Tests.Registration", "parent_id": None, "resource": { - "attributes": { - "cicd.pipeline.name": "JOB", - "cicd.provider.name": "github_actions", - "vcs.ref.head.revision": "3af96aa24f1d32fcfbb7067793cacc6dc0c6b199", - "service.name": "unknown_service", - "telemetry.sdk.language": "python", - "telemetry.sdk.name": "opentelemetry", - "telemetry.sdk.version": anys.ANY_STR, - }, + "attributes": resource_attributes, "schema_url": "", }, "start_time": anys.ANY_DATETIME_STR, @@ -88,15 +94,7 @@ async def test_parse( "name": "Tests.Registration.testCase1", "parent_id": anys.ANY_STR, "resource": { - "attributes": { - "cicd.pipeline.name": "JOB", - "cicd.provider.name": "github_actions", - "vcs.ref.head.revision": "3af96aa24f1d32fcfbb7067793cacc6dc0c6b199", - "service.name": "unknown_service", - "telemetry.sdk.language": "python", - "telemetry.sdk.name": "opentelemetry", - "telemetry.sdk.version": anys.ANY_STR, - }, + "attributes": resource_attributes, "schema_url": "", }, "start_time": anys.ANY_DATETIME_STR, @@ -124,15 +122,7 @@ async def test_parse( "name": "Tests.Registration.testCase2", "parent_id": anys.ANY_STR, "resource": { - "attributes": { - "cicd.pipeline.name": "JOB", - "cicd.provider.name": "github_actions", - "vcs.ref.head.revision": "3af96aa24f1d32fcfbb7067793cacc6dc0c6b199", - "service.name": "unknown_service", - "telemetry.sdk.language": "python", - "telemetry.sdk.name": "opentelemetry", - "telemetry.sdk.version": anys.ANY_STR, - }, + "attributes": resource_attributes, "schema_url": "", }, "start_time": anys.ANY_DATETIME_STR, @@ -163,15 +153,7 @@ async def test_parse( "name": "Tests.Registration.testCase3", "parent_id": anys.ANY_STR, "resource": { - "attributes": { - "cicd.pipeline.name": "JOB", - "cicd.provider.name": "github_actions", - "vcs.ref.head.revision": "3af96aa24f1d32fcfbb7067793cacc6dc0c6b199", - "service.name": "unknown_service", - "telemetry.sdk.language": "python", - "telemetry.sdk.name": "opentelemetry", - "telemetry.sdk.version": anys.ANY_STR, - }, + "attributes": resource_attributes, "schema_url": "", }, "start_time": anys.ANY_DATETIME_STR, @@ -198,15 +180,7 @@ async def test_parse( "name": "Tests.Authentication", "parent_id": None, "resource": { - "attributes": { - "cicd.pipeline.name": "JOB", - "cicd.provider.name": "github_actions", - "vcs.ref.head.revision": "3af96aa24f1d32fcfbb7067793cacc6dc0c6b199", - "service.name": "unknown_service", - "telemetry.sdk.language": "python", - "telemetry.sdk.name": "opentelemetry", - "telemetry.sdk.version": anys.ANY_STR, - }, + "attributes": resource_attributes, "schema_url": "", }, "start_time": anys.ANY_DATETIME_STR, @@ -234,15 +208,7 @@ async def test_parse( "name": "Tests.Authentication.testCase7", "parent_id": anys.ANY_STR, "resource": { - "attributes": { - "cicd.pipeline.name": "JOB", - "cicd.provider.name": "github_actions", - "vcs.ref.head.revision": "3af96aa24f1d32fcfbb7067793cacc6dc0c6b199", - "service.name": "unknown_service", - "telemetry.sdk.language": "python", - "telemetry.sdk.name": "opentelemetry", - "telemetry.sdk.version": anys.ANY_STR, - }, + "attributes": resource_attributes, "schema_url": "", }, "start_time": anys.ANY_DATETIME_STR, @@ -270,15 +236,7 @@ async def test_parse( "name": "Tests.Authentication.testCase8", "parent_id": anys.ANY_STR, "resource": { - "attributes": { - "cicd.pipeline.name": "JOB", - "cicd.provider.name": "github_actions", - "vcs.ref.head.revision": "3af96aa24f1d32fcfbb7067793cacc6dc0c6b199", - "service.name": "unknown_service", - "telemetry.sdk.language": "python", - "telemetry.sdk.name": "opentelemetry", - "telemetry.sdk.version": anys.ANY_STR, - }, + "attributes": resource_attributes, "schema_url": "", }, "start_time": anys.ANY_DATETIME_STR, @@ -309,15 +267,7 @@ async def test_parse( "name": "Tests.Authentication.testCase9", "parent_id": anys.ANY_STR, "resource": { - "attributes": { - "cicd.pipeline.name": "JOB", - "cicd.provider.name": "github_actions", - "vcs.ref.head.revision": "3af96aa24f1d32fcfbb7067793cacc6dc0c6b199", - "service.name": "unknown_service", - "telemetry.sdk.language": "python", - "telemetry.sdk.name": "opentelemetry", - "telemetry.sdk.version": anys.ANY_STR, - }, + "attributes": resource_attributes, "schema_url": "", }, "start_time": anys.ANY_DATETIME_STR, @@ -349,15 +299,7 @@ async def test_parse( "name": "Tests.Permission.testCase10", "parent_id": anys.ANY_STR, "resource": { - "attributes": { - "cicd.pipeline.name": "JOB", - "cicd.provider.name": "github_actions", - "vcs.ref.head.revision": "3af96aa24f1d32fcfbb7067793cacc6dc0c6b199", - "service.name": "unknown_service", - "telemetry.sdk.language": "python", - "telemetry.sdk.name": "opentelemetry", - "telemetry.sdk.version": anys.ANY_STR, - }, + "attributes": resource_attributes, "schema_url": "", }, "start_time": anys.ANY_DATETIME_STR, @@ -384,15 +326,7 @@ async def test_parse( "name": "Tests.Authentication.Login", "parent_id": None, "resource": { - "attributes": { - "cicd.pipeline.name": "JOB", - "cicd.provider.name": "github_actions", - "vcs.ref.head.revision": "3af96aa24f1d32fcfbb7067793cacc6dc0c6b199", - "service.name": "unknown_service", - "telemetry.sdk.language": "python", - "telemetry.sdk.name": "opentelemetry", - "telemetry.sdk.version": anys.ANY_STR, - }, + "attributes": resource_attributes, "schema_url": "", }, "start_time": anys.ANY_DATETIME_STR, @@ -420,15 +354,7 @@ async def test_parse( "name": "Tests.Authentication.Login.testCase4", "parent_id": anys.ANY_STR, "resource": { - "attributes": { - "cicd.pipeline.name": "JOB", - "cicd.provider.name": "github_actions", - "vcs.ref.head.revision": "3af96aa24f1d32fcfbb7067793cacc6dc0c6b199", - "service.name": "unknown_service", - "telemetry.sdk.language": "python", - "telemetry.sdk.name": "opentelemetry", - "telemetry.sdk.version": anys.ANY_STR, - }, + "attributes": resource_attributes, "schema_url": "", }, "start_time": anys.ANY_DATETIME_STR, @@ -459,15 +385,7 @@ async def test_parse( "name": "Tests.Authentication.Login.testCase5", "parent_id": anys.ANY_STR, "resource": { - "attributes": { - "service.name": "unknown_service", - "telemetry.sdk.language": "python", - "telemetry.sdk.name": "opentelemetry", - "telemetry.sdk.version": anys.ANY_STR, - "cicd.pipeline.name": "JOB", - "cicd.provider.name": "github_actions", - "vcs.ref.head.revision": "3af96aa24f1d32fcfbb7067793cacc6dc0c6b199", - }, + "attributes": resource_attributes, "schema_url": "", }, "start_time": anys.ANY_DATETIME_STR, @@ -495,15 +413,7 @@ async def test_parse( "name": "Tests.Authentication.Login.testCase6", "parent_id": anys.ANY_STR, "resource": { - "attributes": { - "cicd.pipeline.name": "JOB", - "cicd.provider.name": "github_actions", - "vcs.ref.head.revision": "3af96aa24f1d32fcfbb7067793cacc6dc0c6b199", - "service.name": "unknown_service", - "telemetry.sdk.language": "python", - "telemetry.sdk.name": "opentelemetry", - "telemetry.sdk.version": anys.ANY_STR, - }, + "attributes": resource_attributes, "schema_url": "", }, "start_time": anys.ANY_DATETIME_STR, diff --git a/mergify_cli/tests/ci/test_upload.py b/mergify_cli/tests/ci/test_upload.py index d8dc989f..70a2f9c4 100644 --- a/mergify_cli/tests/ci/test_upload.py +++ b/mergify_cli/tests/ci/test_upload.py @@ -63,11 +63,13 @@ async def test_junit_upload( captured = capsys.readouterr() if env["GITHUB_ACTIONS"] == "true": - assert re.search( - r"^::notice title=Mergify CI::MERGIFY_TRACE_ID=\d+", + matched = re.search( + r"^::notice title=Mergify CI::MERGIFY_TEST_ROOT_SPAN_ID=(\d+)", captured.out, re.MULTILINE, ) + assert matched is not None + assert int(matched.group(1)) > 0 assert "🎉 File(s) uploaded" in captured.out