diff --git a/integrations/jenkins/.port/resources/blueprints.json b/integrations/jenkins/.port/resources/blueprints.json index b8c4b92742..6ea068a6e5 100644 --- a/integrations/jenkins/.port/resources/blueprints.json +++ b/integrations/jenkins/.port/resources/blueprints.json @@ -60,12 +60,14 @@ "enum": [ "SUCCESS", "FAILURE", - "UNSTABLE" + "UNSTABLE", + "ABORTED" ], "enumColors": { "SUCCESS": "green", "FAILURE": "red", - "UNSTABLE": "yellow" + "UNSTABLE": "yellow", + "ABORTED": "darkGray" } }, "buildUrl": { diff --git a/integrations/jenkins/.port/resources/port-app-config.yaml b/integrations/jenkins/.port/resources/port-app-config.yaml index 850e330bc4..ce83c3c260 100644 --- a/integrations/jenkins/.port/resources/port-app-config.yaml +++ b/integrations/jenkins/.port/resources/port-app-config.yaml @@ -7,7 +7,7 @@ resources: port: entity: mappings: - identifier: .url | split("://")[1] | sub("^.*?/"; "") | gsub("%20"; "-") | gsub("/"; "-") | .[:-1] + identifier: .url | split("://")[1] | sub("^.*?/"; "") | gsub("%20"; "-") | gsub("%252F"; "-") | gsub("/"; "-") | .[:-1] title: .fullName blueprint: '"jenkinsJob"' properties: @@ -22,7 +22,7 @@ resources: port: entity: mappings: - identifier: .url | split("://")[1] | sub("^.*?/"; "") | gsub("%20"; "-") | gsub("/"; "-") | .[:-1] + identifier: .url | split("://")[1] | sub("^.*?/"; "") | gsub("%20"; "-") | gsub("%252F"; "-") | gsub("/"; "-") | .[:-1] title: .displayName blueprint: '"jenkinsBuild"' properties: @@ -31,8 +31,8 @@ resources: buildDuration: .duration timestamp: '.timestamp / 1000 | todate' relations: - parentJob: .url | split("://")[1] | sub("^.*?/"; "") | gsub("%20"; "-") | gsub("/"; "-") | .[:-1] | gsub("-[0-9]+$"; "") - previousBuild: .previousBuild.url | split("://")[1] | sub("^.*?/"; "") | gsub("%20"; "-") | gsub("/"; "-") | .[:-1] + parentJob: .url | split("://")[1] | sub("^.*?/"; "") | gsub("%20"; "-") | gsub("%252F"; "-") | gsub("/"; "-") | .[:-1] | gsub("-[0-9]+$"; "") + previousBuild: .previousBuild.url | split("://")[1] | sub("^.*?/"; "") | gsub("%20"; "-") | gsub("%252F"; "-") | gsub("/"; "-") | .[:-1] - kind: user selector: query: "true" diff --git a/integrations/jenkins/.port/spec.yaml b/integrations/jenkins/.port/spec.yaml index 53050fca81..5433c81c74 100644 --- a/integrations/jenkins/.port/spec.yaml +++ b/integrations/jenkins/.port/spec.yaml @@ -9,6 +9,7 @@ features: - kind: job - kind: build - kind: user + - kind: stage configurations: - name: jenkinsHost description: The base URL of your Jenkins server. This should be the address you use to access the Jenkins dashboard in your browser (e.g., "https://your-jenkins-server.com"). diff --git a/integrations/jenkins/CHANGELOG.md b/integrations/jenkins/CHANGELOG.md index 721d172994..a65279eebe 100644 --- a/integrations/jenkins/CHANGELOG.md +++ b/integrations/jenkins/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.1.60 (2024-09-25) + +### Improvements + +- Added the `stage` kind to the integration to bring stages information about Jenkins builds ## 0.1.61 (2024-10-01) @@ -81,119 +86,94 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 0.1.52 (2024-08-28) - ### Improvements - Bumped ocean version to ^0.10.4 (#1) - ## 0.1.51 (2024-08-28) - ### Improvements - Bumped ocean version to ^0.10.3 (#1) - ## 0.1.50 (2024-08-26) - ### Improvements - Bumped ocean version to ^0.10.2 (#1) - ## 0.1.49 (2024-08-26) - ### Improvements - Bumped ocean version to ^0.10.1 (#1) - ## 0.1.48 (2024-08-22) - ### Improvements - Bumped ocean version to ^0.10.0 (#1) - ## 0.1.47 (2024-08-20) - ### Improvements - Bumped ocean version to ^0.9.14 (#1) - ## 0.1.46 (2024-08-13) - ### Improvements - Bumped ocean version to ^0.9.13 (#1) - ## 0.1.45 (2024-08-11) - ### Improvements - Bumped ocean version to ^0.9.12 (#1) - ## 0.1.44 (2024-08-05) - ### Improvements - Bumped ocean version to ^0.9.11 (#1) - ## 0.1.43 (2024-08-04) - ### Improvements - Bumped ocean version to ^0.9.10 (#1) - ## 0.1.42 (2024-07-31) ### Improvements - Upgraded integration dependencies (#1) - ## 0.1.41 (2024-07-31) ### Improvements - Bumped ocean version to ^0.9.7 (#1) - ## 0.1.40 (2024-07-31) ### Improvements - Bumped ocean version to ^0.9.6 (#1) - ## 0.1.39 (2024-07-24) ### Improvements - Bumped ocean version to ^0.9.5 - ## 0.1.38 (2024-07-10) ### Improvements - Bumped ocean version to ^0.9.4 (#1) - ## 0.1.37 (2024-07-09) ### Improvements @@ -206,77 +186,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bumped ocean version to ^0.9.3 (#1) - ## 0.1.35 (2024-07-07) ### Improvements - Bumped ocean version to ^0.9.2 (#1) - ## 0.1.34 (2024-06-23) ### Improvements - Bumped ocean version to ^0.9.1 (#1) - ## 0.1.33 (2024-06-19) ### Improvements - Bumped ocean version to ^0.9.0 (#1) - ## 0.1.32 (2024-06-16) ### Improvements - Bumped ocean version to ^0.8.0 (#1) - ## 0.1.31 (2024-06-13) ### Improvements - Bumped ocean version to ^0.7.1 (#1) - ## 0.1.30 (2024-06-13) ### Improvements - Bumped ocean version to ^0.7.0 (#1) - ## 0.1.29 (2024-06-10) ### Improvements - Bumped ocean version to ^0.6.0 (#1) - ## 0.1.28 (2024-06-05) ### Improvements - Bumped ocean version to ^0.5.27 (#1) - ## 0.1.27 (2024-06-03) ### Improvements - Bumped ocean version to ^0.5.25 (#1) - ## 0.1.26 (2024-06-02) ### Improvements - Bumped ocean version to ^0.5.24 (#1) - ## 0.1.25 (2024-05-30) ### Improvements @@ -284,21 +253,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bumped ocean version to ^0.5.23 (#1) - Updated the base image used in the Dockerfile that is created during integration scaffolding from `python:3.11-slim-buster` to `python:3.11-slim-bookworm` - ## 0.1.24 (2024-05-29) ### Improvements - Bumped ocean version to ^0.5.22 (#1) - ## 0.1.23 (2024-05-26) ### Improvements - Bumped ocean version to ^0.5.21 (#1) - ## 0.1.22 (2024-05-26) ### Improvements @@ -306,140 +272,120 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bumped ocean version to ^0.5.20 (#1) - Removed the config.yaml file due to unused overrides - ## 0.1.21 (2024-05-16) ### Improvements - Bumped ocean version to ^0.5.19 (#1) - ## 0.1.20 (2024-05-12) ### Improvements - Bumped ocean version to ^0.5.18 (#1) - ## 0.1.19 (2024-05-01) ### Improvements - Bumped ocean version to ^0.5.17 (#1) - ## 0.1.18 (2024-05-01) ### Improvements - Bumped ocean version to ^0.5.16 (#1) - ## 0.1.17 (2024-04-30) ### Improvements - Bumped ocean version to ^0.5.15 (#1) - ## 0.1.16 (2024-04-24) ### Improvements - Bumped ocean version to ^0.5.14 (#1) - ## 0.1.15 (2024-04-17) ### Improvements - Bumped ocean version to ^0.5.12 (#1) - ## 0.1.14 (2024-04-11) ### Improvements - Bumped ocean version to ^0.5.11 (#1) - ## 0.1.13 (2024-04-10) ### Improvements - Bumped ocean version to ^0.5.10 (#1) - ## 0.1.12 (2024-04-01) ### Improvements - Bumped ocean version to ^0.5.9 (#1) - ## 0.1.11 (2024-03-28) ### Improvements - Bumped ocean version to ^0.5.8 (#1) - ## 0.1.10 (2024-03-20) ### Improvements - Bumped ocean version to ^0.5.7 (#1) - ## 0.1.9 (2024-03-17) ### Improvements - Bumped ocean version to ^0.5.6 (#1) - ## 0.1.8 (2024-03-06) ### Improvements - Bumped ocean version to ^0.5.5 (#1) - ## 0.1.7 (2024-03-03) ### Improvements - Bumped ocean version to ^0.5.4 (#1) - ## 0.1.6 (2024-03-03) ### Improvements - Bumped ocean version to ^0.5.3 (#1) - ## 0.1.5 (2024-02-21) ### Improvements - Bumped ocean version to ^0.5.2 (#1) - ## 0.1.4 (2024-02-20) ### Improvements - Bumped ocean version to ^0.5.1 (#1) - ## 0.1.3 (2024-02-18) ### Improvements - Bumped ocean version to ^0.5.0 (#1) - ## 0.1.2 (2024-01-27) ### Features diff --git a/integrations/jenkins/client.py b/integrations/jenkins/client.py index df0817153e..8ce5cd4374 100644 --- a/integrations/jenkins/client.py +++ b/integrations/jenkins/client.py @@ -1,6 +1,6 @@ from enum import StrEnum from typing import Any, AsyncGenerator, Optional -from urllib.parse import urlparse +from urllib.parse import urlparse, urljoin import httpx from loguru import logger @@ -14,6 +14,7 @@ class ResourceKey(StrEnum): JOBS = "jobs" BUILDS = "builds" + STAGES = "stages" class JenkinsClient: @@ -42,9 +43,43 @@ async def get_builds(self) -> AsyncGenerator[list[dict[str, Any]], None]: async for _jobs in self.fetch_resources(ResourceKey.BUILDS): builds = [build for job in _jobs for build in job.get("builds", [])] + logger.debug(f"Builds received {builds}") event.attributes.setdefault(ResourceKey.BUILDS, []).extend(builds) yield builds + async def _get_build_stages(self, build_url: str) -> list[dict[str, Any]]: + response = await self.client.get(f"{build_url}/wfapi/describe") + response.raise_for_status() + stages = response.json().get("stages", []) + return stages + + async def _get_job_builds(self, job_url: str) -> AsyncGenerator[Any, None]: + job_details = await self.get_single_resource(job_url) + if job_details.get("buildable"): + yield job_details.get("builds") + + job = {"url": job_url} + async for _jobs in self.fetch_resources(ResourceKey.BUILDS, job): + builds = [build for job in _jobs for build in job.get("builds", [])] + yield builds + + async def get_stages( + self, job_url: str + ) -> AsyncGenerator[list[dict[str, Any]], None]: + async for builds in self._get_job_builds(job_url): + stages: list[dict[str, Any]] = [] + for build in builds: + build_url = build["url"] + try: + logger.info(f"Getting stages for build {build_url}") + build_stages = await self._get_build_stages(build_url) + stages.extend(build_stages) + yield build_stages + except Exception as e: + logger.error( + f"Failed to get stages for build {build_url}: {e.args[0]}" + ) + async def fetch_resources( self, resource: str, parent_job: Optional[dict[str, Any]] = None ) -> AsyncGenerator[list[dict[str, Any]], None]: @@ -56,10 +91,13 @@ async def fetch_resources( while True: params = self._build_api_params(resource, page_size, page) base_url = self._build_base_url(parent_job) + logger.info(f"Fetching {resource} from {base_url} with params {params}") job_response = await self.client.get(f"{base_url}/api/json", params=params) job_response.raise_for_status() - jobs = job_response.json()["jobs"] + logger.debug(f"Fetched {job_response.json()}") + jobs = job_response.json().get("jobs", []) + logger.info(f"Fetched {len(jobs)} jobs") if not jobs: break @@ -116,9 +154,14 @@ async def get_single_resource(self, resource_url: str) -> dict[str, Any]: Job: job/JobName/ Build: job/JobName/34/ """ - response = await self.client.get( - f"{self.jenkins_base_url}/{resource_url}api/json" - ) + # Ensure resource_url ends with a slash + if not resource_url.endswith("/"): + resource_url += "/" + + # Construct the full URL using urljoin + fetch_url = urljoin(self.jenkins_base_url, f"{resource_url}api/json") + + response = await self.client.get(fetch_url) response.raise_for_status() return response.json() diff --git a/integrations/jenkins/examples/blueprints.json b/integrations/jenkins/examples/blueprints.json new file mode 100644 index 0000000000..7e8735bd3f --- /dev/null +++ b/integrations/jenkins/examples/blueprints.json @@ -0,0 +1,60 @@ +[ + { + "identifier": "jenkinsStage", + "description": "This blueprint represents a stage in a Jenkins build", + "title": "Jenkins Stage", + "icon": "Jenkins", + "schema": { + "properties": { + "status": { + "type": "string", + "title": "Stage Status", + "enum": [ + "SUCCESS", + "FAILURE", + "UNSTABLE", + "ABORTED", + "IN_PROGRESS", + "NOT_BUILT", + "PAUSED_PENDING_INPUT" + ], + "enumColors": { + "SUCCESS": "green", + "FAILURE": "red", + "UNSTABLE": "yellow", + "ABORTED": "darkGray", + "IN_PROGRESS": "blue", + "NOT_BUILT": "lightGray", + "PAUSED_PENDING_INPUT": "orange" + } + }, + "startTimeMillis": { + "type": "number", + "title": "Start Time (ms)", + "description": "Timestamp in milliseconds when the stage started" + }, + "durationMillis": { + "type": "number", + "title": "Duration (ms)", + "description": "Duration of the stage in milliseconds" + }, + "stageUrl": { + "type": "string", + "title": "Stage URL", + "description": "URL to the stage" + } + }, + "required": [] + }, + "mirrorProperties": {}, + "calculationProperties": {}, + "relations": { + "parentBuild": { + "title": "Jenkins Build", + "target": "jenkinsBuild", + "required": true, + "many": false + } + } + } +] \ No newline at end of file diff --git a/integrations/jenkins/examples/mappings.yml b/integrations/jenkins/examples/mappings.yml new file mode 100644 index 0000000000..44fc3aa04a --- /dev/null +++ b/integrations/jenkins/examples/mappings.yml @@ -0,0 +1,70 @@ +deleteDependentEntities: true +createMissingRelatedEntities: true +enableMergeEntity: true +resources: + - kind: stage + selector: + query: 'true' + jobUrl: http://localhost:8080/job/limbopay/job/Limbo%20Core/job/main + port: + entity: + mappings: + identifier: >- + ._links.self.href | sub("^.*?/"; "") | gsub("%20"; "-") | + gsub("%252F"; "-") | gsub("/"; "-") + title: .name + blueprint: '"jenkinsStage"' + properties: + status: .status + startTimeMillis: .startTimeMillis + durationMillis: .durationMillis + stageUrl: env.OCEAN__INTEGRATION__CONFIG__JENKINS_HOST + ._links.self.href + relations: + parentBuild: >- + ._links.self.href | sub("/execution/node/[0-9]+/wfapi/describe$"; + "") | sub("^.*?/"; "") | gsub("%20"; "-") | gsub("%252F"; "-") | + gsub("/"; "-") + - kind: stage + selector: + query: 'true' + jobUrl: http://localhost:8080/job/Phalbert/job/airframe-react + port: + entity: + mappings: + identifier: >- + ._links.self.href | sub("^.*?/"; "") | gsub("%20"; "-") | + gsub("%252F"; "-") | gsub("/"; "-") + title: .name + blueprint: '"jenkinsStage"' + properties: + status: .status + startTimeMillis: .startTimeMillis + durationMillis: .durationMillis + stageUrl: env.OCEAN__INTEGRATION__CONFIG__JENKINS_HOST + ._links.self.href + relations: + parentBuild: >- + ._links.self.href | sub("/execution/node/[0-9]+/wfapi/describe$"; + "") | sub("^.*?/"; "") | gsub("%20"; "-") | gsub("%252F"; "-") | + gsub("/"; "-") + - kind: stage + selector: + query: 'true' + jobUrl: http://localhost:8080/job/Phalbert/job/autoshop_api + port: + entity: + mappings: + identifier: >- + ._links.self.href | sub("^.*?/"; "") | gsub("%20"; "-") | + gsub("%252F"; "-") | gsub("/"; "-") + title: .name + blueprint: '"jenkinsStage"' + properties: + status: .status + startTimeMillis: .startTimeMillis + durationMillis: .durationMillis + stageUrl: env.OCEAN__INTEGRATION__CONFIG__JENKINS_HOST + ._links.self.href + relations: + parentBuild: >- + ._links.self.href | sub("/execution/node/[0-9]+/wfapi/describe$"; + "") | sub("^.*?/"; "") | gsub("%20"; "-") | gsub("%252F"; "-") | + gsub("/"; "-") diff --git a/integrations/jenkins/examples/stage.entity.json b/integrations/jenkins/examples/stage.entity.json new file mode 100644 index 0000000000..e61df7d112 --- /dev/null +++ b/integrations/jenkins/examples/stage.entity.json @@ -0,0 +1,20 @@ +{ + "identifier": "job-Phalbert-job-salesdash-job-master-229-execution-node-17-wfapi-describe", + "title": "Declarative: Post Actions", + "icon": null, + "blueprint": "jenkinsStage", + "team": [], + "properties": { + "status": "SUCCESS", + "startTimeMillis": 1717073272012, + "durationMillis": 26, + "stageUrl": "http://localhost:8080/job/Phalbert/job/salesdash/job/master/229/execution/node/17/wfapi/describe" + }, + "relations": { + "parentBuild": "job-Phalbert-job-salesdash-job-master-229" + }, + "createdAt": "2024-08-28T10:27:33.549Z", + "createdBy": "", + "updatedAt": "2024-08-28T10:27:30.274Z", + "updatedBy": "" +} diff --git a/integrations/jenkins/examples/stage.response.json b/integrations/jenkins/examples/stage.response.json new file mode 100644 index 0000000000..2c7b617de5 --- /dev/null +++ b/integrations/jenkins/examples/stage.response.json @@ -0,0 +1,15 @@ +{ + "_links": { + "self": { + "href": "/job/Phalbert/job/salesdash/job/master/227/execution/node/17/wfapi/describe" + } + }, + "id": "17", + "name": "Declarative: Post Actions", + "execNode": "", + "status": "SUCCESS", + "startTimeMillis": 1717073271079, + "durationMillis": 51, + "pauseDurationMillis": 0, + "__fullUrl": "http://localhost:8080/job/Phalbert/job/salesdash/job/master/227/execution/node/17/wfapi/describe" +} diff --git a/integrations/jenkins/integration.py b/integrations/jenkins/integration.py new file mode 100644 index 0000000000..ab43ebf7b1 --- /dev/null +++ b/integrations/jenkins/integration.py @@ -0,0 +1,9 @@ +from port_ocean.core.handlers.port_app_config.api import APIPortAppConfig +from port_ocean.core.integrations.base import BaseIntegration + +from overrides import JenkinsPortAppConfig + + +class JenkinsIntegration(BaseIntegration): + class AppConfigHandlerClass(APIPortAppConfig): + CONFIG_CLASS = JenkinsPortAppConfig diff --git a/integrations/jenkins/main.py b/integrations/jenkins/main.py index 7290fbafbc..d267bc2937 100644 --- a/integrations/jenkins/main.py +++ b/integrations/jenkins/main.py @@ -1,16 +1,21 @@ -from typing import Any +from typing import Any, cast from loguru import logger from enum import StrEnum from client import JenkinsClient from port_ocean.context.ocean import ocean +from port_ocean.context.event import event + from port_ocean.core.ocean_types import ASYNC_GENERATOR_RESYNC_TYPE +from overrides import JenkinStagesResourceConfig + class ObjectKind(StrEnum): JOB = "job" BUILD = "build" USER = "user" + STAGE = "stage" @staticmethod def get_object_kind_for_event(obj_type: str) -> str | None: @@ -23,6 +28,7 @@ def get_object_kind_for_event(obj_type: str) -> str | None: def init_client() -> JenkinsClient: + logger.info(f"Initializing JenkinsClient {ocean.integration_config}") return JenkinsClient( ocean.integration_config["jenkins_host"], ocean.integration_config["jenkins_user"], @@ -57,6 +63,32 @@ async def on_resync_users(kind: str) -> ASYNC_GENERATOR_RESYNC_TYPE: yield users +@ocean.on_resync(ObjectKind.STAGE) +async def on_resync_stages(kind: str) -> ASYNC_GENERATOR_RESYNC_TYPE: + jenkins_client = init_client() + stages_count = 0 + max_stages = 10000 + + stages_selector = cast(JenkinStagesResourceConfig, event.resource_config) + job_url = stages_selector.selector.job_url + + logger.info(f"Syncing stages for job {job_url}") + + async for stages in jenkins_client.get_stages(job_url): + logger.info(f"Received batch with {len(stages)} stages") + if stages_count + len(stages) > max_stages: + stages = stages[: max_stages - stages_count] + yield stages + logger.warning( + f"Reached the maximum limit of {max_stages} stages. Skipping the remaining stages." + ) + return + stages_count += len(stages) + yield stages + + logger.info(f"Total stages synced: {stages_count}") + + @ocean.router.post("/events") async def handle_events(event: dict[str, Any]) -> dict[str, bool]: jenkins_client = init_client() diff --git a/integrations/jenkins/overrides.py b/integrations/jenkins/overrides.py new file mode 100644 index 0000000000..44f0529a53 --- /dev/null +++ b/integrations/jenkins/overrides.py @@ -0,0 +1,23 @@ +import typing + +from port_ocean.core.handlers.port_app_config.models import ( + ResourceConfig, + PortAppConfig, + Selector, +) +from pydantic import Field + + +class JenkinStagesResourceConfig(ResourceConfig): + class JenkinStageSelector(Selector): + query: str + job_url: str = Field(alias="jobUrl", required=True) + + kind: typing.Literal["stage"] + selector: JenkinStageSelector + + +class JenkinsPortAppConfig(PortAppConfig): + resources: list[JenkinStagesResourceConfig | ResourceConfig] = Field( + default_factory=list + ) diff --git a/integrations/jenkins/tests/test_client.py b/integrations/jenkins/tests/test_client.py new file mode 100644 index 0000000000..d55317a58e --- /dev/null +++ b/integrations/jenkins/tests/test_client.py @@ -0,0 +1,182 @@ +import pytest +from unittest.mock import AsyncMock, patch +from client import JenkinsClient +from typing import Any, AsyncGenerator, Dict, List + + +@pytest.mark.asyncio +@patch( + "port_ocean.context.ocean.PortOceanContext.integration_config", + new_callable=AsyncMock, +) +@patch("port_ocean.utils.async_http.OceanAsyncClient", new_callable=AsyncMock) +async def test_get_stages( + mock_ocean_client: AsyncMock, mock_integration_config: AsyncMock +) -> None: + # Mock integration config + mock_integration_config.return_value = { + "jenkins_host": "http://localhost:8080", + "jenkins_user": "hpal[REDACTED]", + "jenkins_token": "11b053[REDACTED]", + } + + # Mock data + job_url = "http://jenkins.example.com/job/test-job/" + build_url = "http://jenkins.example.com/job/test-job/1/" + mock_stages = [ + { + "_links": { + "self": {"href": "/job/test-job/1/execution/node/6/wfapi/describe"} + }, + "id": "6", + "name": "Declarative: Checkout SCM", + "execNode": "", + "status": "SUCCESS", + "startTimeMillis": 1717068226152, + "durationMillis": 1173, + "pauseDurationMillis": 0, + }, + { + "_links": { + "self": {"href": "/job/test-job/1/execution/node/17/wfapi/describe"} + }, + "id": "17", + "name": "Declarative: Post Actions", + "execNode": "", + "status": "SUCCESS", + "startTimeMillis": 1717068227381, + "durationMillis": 25, + "pauseDurationMillis": 0, + }, + ] + + # Create a JenkinsClient instance + with patch("client.http_async_client", new=mock_ocean_client): + client = JenkinsClient("http://jenkins.example.com", "user", "token") + + # Mock the necessary methods + async def mock_get_job_builds( + job_url: str, + ) -> AsyncGenerator[List[Dict[str, Any]], None]: + yield [{"url": build_url}] + + with ( + patch.object( + client, "_get_job_builds", side_effect=mock_get_job_builds + ) as mock_get_job_builds, + patch.object( + client, "_get_build_stages", new_callable=AsyncMock + ) as mock_get_build_stages, + ): + + # Set up the mock returns + mock_get_build_stages.return_value = mock_stages + + # Call the method and collect results + stages = [] + async for stage_batch in client.get_stages(job_url): + stages.extend(stage_batch) + + # Assertions + assert stages == mock_stages + mock_get_job_builds.assert_called_once_with(job_url) + mock_get_build_stages.assert_called_once_with(build_url) + + +@pytest.mark.asyncio +@patch( + "port_ocean.context.ocean.PortOceanContext.integration_config", + new_callable=AsyncMock, +) +@patch("port_ocean.utils.async_http.OceanAsyncClient", new_callable=AsyncMock) +async def test_get_stages_nested_jobs( + mock_ocean_client: AsyncMock, mock_integration_config: AsyncMock +) -> None: + # Mock integration config + mock_integration_config.return_value = { + "jenkins_host": "http://localhost:8080", + "jenkins_user": "hpal[REDACTED]", + "jenkins_token": "11b053[REDACTED]", + } + + # Mock data + parent_job_url = "http://jenkins.example.com/job/parent-job/" + child_job_url = "http://jenkins.example.com/job/parent-job/job/child-job/" + build_url = "http://jenkins.example.com/job/parent-job/job/child-job/1/" + mock_stages = [ + { + "id": "6", + "name": "Build", + "status": "SUCCESS", + "startTimeMillis": 1717068226152, + "durationMillis": 1173, + "pauseDurationMillis": 0, + }, + { + "id": "17", + "name": "Test", + "status": "SUCCESS", + "startTimeMillis": 1717068227381, + "durationMillis": 25, + "pauseDurationMillis": 0, + }, + ] + + # Create a JenkinsClient instance + with patch("client.http_async_client", new=mock_ocean_client): + client = JenkinsClient("http://jenkins.example.com", "user", "token") + + # Mock the necessary methods + async def mock_get_single_resource(resource_url: str) -> Dict[str, Any]: + if resource_url == parent_job_url: + return {"buildable": False, "jobs": [{"url": child_job_url}]} + elif resource_url == child_job_url: + return {"buildable": True, "builds": [{"url": build_url}]} + else: + return {"buildable": False} + + async def mock_fetch_resources( + resource: str, parent_job: str | None = None + ) -> AsyncGenerator[List[Dict[str, Any]], None]: + if parent_job is None: + yield [ + { + "url": child_job_url, + "buildable": False, + "jobs": [{"url": child_job_url}], + } + ] + else: + yield [ + { + "url": child_job_url, + "buildable": True, + "builds": [{"url": build_url}], + } + ] + + with ( + patch.object( + client, "get_single_resource", side_effect=mock_get_single_resource + ) as mock_get_single_resource, + patch.object( + client, "fetch_resources", side_effect=mock_fetch_resources + ) as mock_fetch_resources, + patch.object( + client, "_get_build_stages", new_callable=AsyncMock + ) as mock_get_build_stages, + ): + + # Set up the mock returns + mock_get_build_stages.return_value = mock_stages + + # Call the method and collect results + stages = [] + async for stage_batch in client.get_stages(parent_job_url): + stages.extend(stage_batch) + + # Assertions + assert stages == mock_stages + mock_get_single_resource.assert_called_with(parent_job_url) + mock_fetch_resources.assert_called() + mock_get_build_stages.assert_called_once_with(build_url) diff --git a/integrations/jenkins/tests/test_sample.py b/integrations/jenkins/tests/test_sample.py deleted file mode 100644 index dc80e299c8..0000000000 --- a/integrations/jenkins/tests/test_sample.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_example() -> None: - assert 1 == 1 diff --git a/integrations/wiz/CHANGELOG.md b/integrations/wiz/CHANGELOG.md index 3731be46e1..66ccfa7d01 100644 --- a/integrations/wiz/CHANGELOG.md +++ b/integrations/wiz/CHANGELOG.md @@ -33,7 +33,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 0.1.59 (2024-09-17) - ### Improvements - Bumped ocean version to ^0.10.11 @@ -47,10 +46,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 0.1.57 (2024-09-12) + ### Improvements - Bumped ocean version to ^0.10.10 (#1) + ## 0.1.56 (2024-09-05) ### Improvements