From 97f93331531d36dd9d5533f028148296f613fc95 Mon Sep 17 00:00:00 2001 From: Wei Yuan Cho Date: Tue, 22 Aug 2023 16:53:52 +0100 Subject: [PATCH 1/2] #309 JobList API to support Tree and Depth --- .../cdancy/jenkins/rest/domain/job/Job.java | 1 - .../jenkins/rest/domain/job/JobListTree.java | 58 +++++++++++ .../cdancy/jenkins/rest/features/JobsApi.java | 9 ++ .../rest/features/JobsApiLiveTest.java | 96 +++++++++++++++++-- .../rest/features/JobsApiMockTest.java | 65 +++++++++++++ src/test/resources/getJobListByDepth.json | 43 +++++++++ .../jobsInJenkinsFolderByFullName.json | 9 ++ src/test/resources/nestedJobList.json | 19 ++++ 8 files changed, 291 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/cdancy/jenkins/rest/domain/job/JobListTree.java create mode 100644 src/test/resources/getJobListByDepth.json create mode 100644 src/test/resources/jobsInJenkinsFolderByFullName.json create mode 100644 src/test/resources/nestedJobList.json diff --git a/src/main/java/com/cdancy/jenkins/rest/domain/job/Job.java b/src/main/java/com/cdancy/jenkins/rest/domain/job/Job.java index 090b4a14..f25aac60 100644 --- a/src/main/java/com/cdancy/jenkins/rest/domain/job/Job.java +++ b/src/main/java/com/cdancy/jenkins/rest/domain/job/Job.java @@ -11,7 +11,6 @@ public abstract class Job { public abstract String clazz(); public abstract String name(); - public abstract String url(); @Nullable diff --git a/src/main/java/com/cdancy/jenkins/rest/domain/job/JobListTree.java b/src/main/java/com/cdancy/jenkins/rest/domain/job/JobListTree.java new file mode 100644 index 00000000..a51fa063 --- /dev/null +++ b/src/main/java/com/cdancy/jenkins/rest/domain/job/JobListTree.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cdancy.jenkins.rest.domain.job; + +import java.util.List; + +import com.google.auto.value.AutoValue; +import org.jclouds.javax.annotation.Nullable; +import org.jclouds.json.SerializedNames; + +@AutoValue +public abstract class JobListTree { + + @Nullable + public abstract String clazz(); + + @Nullable + public abstract String name(); + + @Nullable + public abstract String fullName(); + + @Nullable + public abstract List jobs(); + + @Nullable + public abstract String color(); + + @Nullable + public abstract String url(); + + @SerializedNames({"_class", "name", "fullName", "jobs", "color", "url"}) + public static JobListTree create( + @Nullable String clazz, + @Nullable String name, + @Nullable String fullName, + @Nullable List jobs, + @Nullable String color, + @Nullable String url + ) { + return new AutoValue_JobListTree(clazz, name, fullName, jobs, color, url); + } +} diff --git a/src/main/java/com/cdancy/jenkins/rest/features/JobsApi.java b/src/main/java/com/cdancy/jenkins/rest/features/JobsApi.java index b1f91495..721570c5 100644 --- a/src/main/java/com/cdancy/jenkins/rest/features/JobsApi.java +++ b/src/main/java/com/cdancy/jenkins/rest/features/JobsApi.java @@ -62,6 +62,15 @@ public interface JobsApi { @GET JobList jobList(@PathParam("folderPath") @ParamParser(FolderPathParser.class) String folderPath); + @Named("jobs:get-jobs-tree") + @Path("{folderPath}api/json") + @Fallback(Fallbacks.NullOnNotFoundOr404.class) + @Consumes(MediaType.APPLICATION_JSON) + @GET + JobListTree jobList(@PathParam("folderPath") @ParamParser(FolderPathParser.class) String folderPath, @Nullable @QueryParam("depth") Integer depth, + @Nullable @QueryParam("tree") String tree); + + @Named("jobs:job-info") @Path("{optionalFolderPath}job/{name}/api/json") @Fallback(Fallbacks.NullOnNotFoundOr404.class) diff --git a/src/test/java/com/cdancy/jenkins/rest/features/JobsApiLiveTest.java b/src/test/java/com/cdancy/jenkins/rest/features/JobsApiLiveTest.java index 61a7648d..025cf7a1 100644 --- a/src/test/java/com/cdancy/jenkins/rest/features/JobsApiLiveTest.java +++ b/src/test/java/com/cdancy/jenkins/rest/features/JobsApiLiveTest.java @@ -16,9 +16,7 @@ */ package com.cdancy.jenkins.rest.features; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import com.cdancy.jenkins.rest.BaseJenkinsApiLiveTest; import com.cdancy.jenkins.rest.domain.common.IntegerResponse; @@ -28,8 +26,10 @@ import com.cdancy.jenkins.rest.domain.plugins.Plugins; import com.cdancy.jenkins.rest.domain.queue.QueueItem; import com.google.common.collect.Lists; +import org.testng.annotations.AfterClass; import org.testng.annotations.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.testng.Assert.*; @Test(groups = "live", testName = "JobsApiLiveTest", singleThreaded = true) @@ -45,6 +45,11 @@ public class JobsApiLiveTest extends BaseJenkinsApiLiveTest { private static final String PIPELINE_JOB_NAME = "PipelineSleep"; private static final String PIPELINE_WITH_ACTION_JOB_NAME = "PipelineAction"; + @AfterClass + public void removeJobs() { + api().delete(null, "DevTest"); + } + @Test public void testCreateJob() { String config = payloadFromResource("/freestyle-project-no-params.xml"); @@ -174,12 +179,18 @@ public void testKillPipelineBuild() throws InterruptedException { assertTrue(success.value()); } - @Test(dependsOnMethods = {"testCreateJob", "testCreateJobForEmptyAndNullParams", "testKillPipelineBuild", "testKillFreeStyleBuild", "testDeleteFolders"}) - public void testGetJobListFromRoot() { + @Test(dependsOnMethods = {"testBuildJob", "testCreateJobForEmptyAndNullParams", "testKillFreeStyleBuild", "testKillPipelineBuild", "testCreateFoldersInJenkins"}) + public void testGetJobListFromRootWithFolders() { JobList output = api().jobList(""); assertNotNull(output); - assertFalse(output.jobs().isEmpty()); - assertEquals(output.jobs().size(), 2); + assertThat(output.jobs()) + .isNotEmpty() + .hasSize(3) + .contains( + Job.create("hudson.model.FreeStyleProject", "DevTest", "http://127.0.0.1:8080/job/DevTest/", "blue"), + Job.create("hudson.model.FreeStyleProject", "JobForEmptyAndNullParams", "http://127.0.0.1:8080/job/JobForEmptyAndNullParams/", "blue"), + Job.create("com.cloudbees.hudson.plugins.folder.Folder", "test-folder", "http://127.0.0.1:8080/job/test-folder/", null) + ); } @Test(dependsOnMethods = "testCreateJob") @@ -439,6 +450,75 @@ public void testGetJobListInFolder() { assertEquals(output.jobs().get(0), Job.create("hudson.model.FreeStyleProject", "JobInFolder", System.getProperty("test.jenkins.endpoint")+"/job/test-folder/job/test-folder-1/job/JobInFolder/", "notbuilt")); } + @Test(dependsOnMethods = "testCreateJobInFolder") + public void testGetJobListByDepth2() { + JobListTree output = api().jobList("", 2, null); + assertNotNull(output); + assertFalse(output.jobs().isEmpty()); + assertEquals(output.jobs().size(), 3); + JobListTree lastRootElement = output.jobs().get(2); + List childJobs = lastRootElement.jobs(); + assertThat(childJobs).isNotEmpty() + .hasSize(1); + assertThat(childJobs.get(0).jobs()).isNotEmpty() + .hasSize(1); + } + + @Test(dependsOnMethods = "testCreateJobInFolder") + public void testGetJobListByDepth1() { + JobListTree output = api().jobList("", 1, null); + assertNotNull(output); + assertFalse(output.jobs().isEmpty()); + assertEquals(output.jobs().size(), 3); + JobListTree lastRootElement = output.jobs().get(2); + List childJobs = lastRootElement.jobs(); + assertThat(childJobs).isNotEmpty() + .hasSize(1); + assertThat(childJobs.get(0).jobs()).isNull(); + } + + @Test(dependsOnMethods = "testCreateJobInFolder") + public void testGetJobListInSelectedFolderWithTreeOnlyGivingFullNameOnCurrentFolder() { + JobListTree output = api().jobList("test-folder/test-folder-1", null, "fullName"); + assertNotNull(output); + assertNull(output.jobs()); + assertEquals(output, JobListTree.create("com.cloudbees.hudson.plugins.folder.Folder", null, "test-folder/test-folder-1", null, null, null)); + } + + @Test(dependsOnMethods = "testCreateJobInFolder") + public void testGetJobListFromRootWithTreeCanReturnNestedJob() { + JobListTree output = api().jobList("", null, "jobs[fullName,jobs[fullName,jobs[fullName]]]"); + assertNotNull(output); + List grandChildJob = Lists.newArrayList(JobListTree.create("hudson.model.FreeStyleProject", null, "test-folder/test-folder-1/JobInFolder", null, null, null)); + JobListTree childJob = JobListTree.create("com.cloudbees.hudson.plugins.folder.Folder", null, "test-folder/test-folder-1", grandChildJob, null, null); + assertThat(output.jobs()) + .isNotEmpty() + .hasSize(3) + .contains( + JobListTree.create("hudson.model.FreeStyleProject", null, "DevTest", null, null, null), + JobListTree.create("hudson.model.FreeStyleProject", null, "JobForEmptyAndNullParams", null, null, null), + JobListTree.create("com.cloudbees.hudson.plugins.folder.Folder", null, "test-folder", Lists.newArrayList(childJob), null, null) + ); + } + + @Test(dependsOnMethods = "testCreateJobInFolder") + public void testGetJobListInFolderWithTreeReturnAll() { + JobListTree output = api().jobList("test-folder/test-folder-1", null, "jobs[*]"); + assertNotNull(output); + assertFalse(output.jobs().isEmpty()); + assertEquals(output.jobs().size(), 1); + assertEquals(output.jobs().get(0), JobListTree.create("hudson.model.FreeStyleProject", "JobInFolder", "test-folder/test-folder-1/JobInFolder", null, "notbuilt", "http://127.0.0.1:8080/job/test-folder/job/test-folder-1/job/JobInFolder/")); + } + + @Test(dependsOnMethods = "testCreateJobInFolder") + public void testGetJobListInFolderWithTreeOnlyGivingNameAndColor() { + JobListTree output = api().jobList("test-folder/test-folder-1", null, "jobs[name,color]"); + assertNotNull(output); + assertFalse(output.jobs().isEmpty()); + assertEquals(output.jobs().size(), 1); + assertEquals(output.jobs().get(0), JobListTree.create("hudson.model.FreeStyleProject", "JobInFolder", null, null, "notbuilt", null)); + } + @Test(dependsOnMethods = "testCreateJobInFolder") public void testUpdateJobConfigInFolder() { String config = payloadFromResource("/freestyle-project.xml"); @@ -567,7 +647,7 @@ public void testGetBuildParametersOfJobForEmptyAndNullParams() { assertTrue(parameters.get(1).value().isEmpty()); } - @Test(dependsOnMethods = { "testGetBuildParametersOfJobForEmptyAndNullParams", "testGetJobListFromRoot"}) + @Test(dependsOnMethods = { "testGetBuildParametersOfJobForEmptyAndNullParams", "testGetJobListFromRootWithFolders"}) public void testDeleteJobForEmptyAndNullParams() { RequestStatus success = api().delete(null, "JobForEmptyAndNullParams"); assertTrue(success.value()); diff --git a/src/test/java/com/cdancy/jenkins/rest/features/JobsApiMockTest.java b/src/test/java/com/cdancy/jenkins/rest/features/JobsApiMockTest.java index a078884e..2c0f66a8 100644 --- a/src/test/java/com/cdancy/jenkins/rest/features/JobsApiMockTest.java +++ b/src/test/java/com/cdancy/jenkins/rest/features/JobsApiMockTest.java @@ -21,6 +21,7 @@ import com.cdancy.jenkins.rest.domain.common.IntegerResponse; import com.cdancy.jenkins.rest.domain.common.RequestStatus; import com.cdancy.jenkins.rest.domain.job.*; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.gson.JsonObject; import okhttp3.mockwebserver.MockResponse; @@ -61,6 +62,70 @@ public void testGetInnerFolderJobList() throws Exception { } } + public void testGetJobListWithDepth() throws Exception { + MockWebServer server = mockWebServer(); + + String body = payloadFromResource("/getJobListByDepth.json"); + server.enqueue(new MockResponse().setBody(body).setResponseCode(200)); + JenkinsApi jenkinsApi = api(server.url("/").url()); + try (jenkinsApi) { + JobsApi api = jenkinsApi.jobsApi(); + JobListTree output = api.jobList("", 0, null); + assertNotNull(output); + assertNotNull(output.jobs()); + assertEquals(output.jobs().size(), 1); + assertEquals(output.jobs().get(0), JobListTree.create("hudson.model.FreeStyleProject", "DevTest", null, null, "notbuilt", "http://localhost:8080/job/DevTest/")); + assertSent(server, "GET", "/api/json", ImmutableMap.of("depth", "0")); + } finally { + server.shutdown(); + } + } + + public void testGetJobListWithTreeByFullName() throws Exception { + MockWebServer server = mockWebServer(); + + String body = payloadFromResource("/jobsInJenkinsFolderByFullName.json"); + server.enqueue(new MockResponse().setBody(body).setResponseCode(200)); + JenkinsApi jenkinsApi = api(server.url("/").url()); + Map queryParams = ImmutableMap.of("tree", "jobs%5BfullName%5D"); + try (jenkinsApi) { + JobsApi api = jenkinsApi.jobsApi(); + JobListTree output = api.jobList("Folder1/Folder 2", null, "jobs[fullName]"); + assertNotNull(output); + assertNotNull(output.jobs()); + assertEquals(output.jobs().size(), 1); + assertEquals(output.jobs().get(0), JobListTree.create("hudson.model.FreeStyleProject", null, "Test Project", null, null, null)); + assertSent(server, "GET", "/job/Folder1/job/Folder%202/api/json", queryParams); + } finally { + server.shutdown(); + } + } + + public void testGetNestedJobList() throws Exception { + MockWebServer server = mockWebServer(); + + String body = payloadFromResource("/nestedJobList.json"); + server.enqueue(new MockResponse().setBody(body).setResponseCode(200)); + JenkinsApi jenkinsApi = api(server.url("/").url()); + Map queryParams = ImmutableMap.of("tree", "jobs%5BfullName,jobs%5BfullName%5D%5D"); + try (jenkinsApi) { + JobsApi api = jenkinsApi.jobsApi(); + JobListTree output = api.jobList("Folder1/Folder 2", null, "jobs[fullName,jobs[fullName]]"); + assertNotNull(output); + assertNotNull(output.jobs()); + assertEquals(output.jobs().size(), 2); + assertEquals(output.jobs().get(0), JobListTree.create("hudson.model.FreeStyleProject", null, "DevTest", null, null, null)); + JobListTree actualFolder = output.jobs().get(1); + assertEquals(actualFolder.clazz(), "com.cloudbees.hudson.plugins.folder.Folder"); + assertEquals(actualFolder.fullName(), "test-folder"); + assertEquals(actualFolder.jobs().size(), 1); + assertEquals(actualFolder.jobs().get(0).fullName(), "test-folder/test-folder-1"); + assertSent(server, "GET", "/job/Folder1/job/Folder%202/api/json", queryParams); + } finally { + server.shutdown(); + } + } + public void testGetRootFolderJobList() throws Exception { MockWebServer server = mockWebServer(); diff --git a/src/test/resources/getJobListByDepth.json b/src/test/resources/getJobListByDepth.json new file mode 100644 index 00000000..07816707 --- /dev/null +++ b/src/test/resources/getJobListByDepth.json @@ -0,0 +1,43 @@ +{ + "_class": "hudson.model.Hudson", + "assignedLabels": [ + { + "name": "built-in" + } + ], + "mode": "NORMAL", + "nodeDescription": "the Jenkins controller's built-in node", + "nodeName": "", + "numExecutors": 2, + "description": "Jenkins REST API Configuration as code test", + "jobs": [ + { + "_class": "hudson.model.FreeStyleProject", + "name": "DevTest", + "url": "http://localhost:8080/job/DevTest/", + "color": "notbuilt" + } + ], + "overallLoad": {}, + "primaryView": { + "_class": "hudson.model.AllView", + "name": "all", + "url": "http://localhost:8080/" + }, + "quietDownReason": null, + "quietingDown": false, + "slaveAgentPort": 50000, + "unlabeledLoad": { + "_class": "jenkins.model.UnlabeledLoadStatistics" + }, + "url": null, + "useCrumbs": true, + "useSecurity": true, + "views": [ + { + "_class": "hudson.model.AllView", + "name": "all", + "url": "http://localhost:8080/" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/jobsInJenkinsFolderByFullName.json b/src/test/resources/jobsInJenkinsFolderByFullName.json new file mode 100644 index 00000000..828f2b17 --- /dev/null +++ b/src/test/resources/jobsInJenkinsFolderByFullName.json @@ -0,0 +1,9 @@ +{ + "_class" : "com.cloudbees.hudson.plugins.folder.Folder", + "jobs" : [ + { + "_class" : "hudson.model.FreeStyleProject", + "fullName" : "Test Project" + } + ] +} diff --git a/src/test/resources/nestedJobList.json b/src/test/resources/nestedJobList.json new file mode 100644 index 00000000..70741d6e --- /dev/null +++ b/src/test/resources/nestedJobList.json @@ -0,0 +1,19 @@ +{ + "_class": "hudson.model.Hudson", + "jobs": [ + { + "_class": "hudson.model.FreeStyleProject", + "fullName": "DevTest" + }, + { + "_class": "com.cloudbees.hudson.plugins.folder.Folder", + "fullName": "test-folder", + "jobs": [ + { + "_class": "com.cloudbees.hudson.plugins.folder.Folder", + "fullName": "test-folder/test-folder-1" + } + ] + } + ] +} \ No newline at end of file From 361ec6ccc4f6ab8683d47a25235f1fa0cf081058 Mon Sep 17 00:00:00 2001 From: Wei Yuan Cho Date: Wed, 6 Sep 2023 10:17:48 +0100 Subject: [PATCH 2/2] Fix repository checkout Dont always check out master --- .github/workflows/github-actions.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/github-actions.yaml b/.github/workflows/github-actions.yaml index 73fdf03d..1612c6db 100644 --- a/.github/workflows/github-actions.yaml +++ b/.github/workflows/github-actions.yaml @@ -11,9 +11,11 @@ jobs: steps: - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" - - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." + - run: echo "🔎 The name of your branch is ${{ github.head_ref || github.ref }} and your repository is ${{ github.repository }}." - name: Check out repository code uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref || github.ref }} - uses: actions/setup-java@v3 with: distribution: temurin