From c1f9c52b76f4018e669dcc0065a1cebeadc76c4c Mon Sep 17 00:00:00 2001
From: Mitul Amin <mitul.github@shravi.dev>
Date: Wed, 22 Jan 2025 01:32:25 +0000
Subject: [PATCH 1/6] Actions.WorkflowJobs Data classes added

Added the classes that will hold the responses from the actions.workflowjobs api's Get and List calls.
---
 .../ListWorkflowJobsQueryParams.java          |  57 ++++++
 .../workflowjobs/WorkflowJobConclusion.java   |  41 +++++
 .../workflowjobs/WorkflowJobResponse.java     | 168 ++++++++++++++++++
 .../workflowjobs/WorkflowJobStatus.java       |  40 +++++
 .../actions/workflowjobs/WorkflowJobStep.java |  67 +++++++
 .../WorkflowJobsResponseList.java             |  51 ++++++
 6 files changed, 424 insertions(+)
 create mode 100644 src/main/java/com/spotify/github/v3/actions/workflowjobs/ListWorkflowJobsQueryParams.java
 create mode 100644 src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobConclusion.java
 create mode 100644 src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobResponse.java
 create mode 100644 src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobStatus.java
 create mode 100644 src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobStep.java
 create mode 100644 src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobsResponseList.java

diff --git a/src/main/java/com/spotify/github/v3/actions/workflowjobs/ListWorkflowJobsQueryParams.java b/src/main/java/com/spotify/github/v3/actions/workflowjobs/ListWorkflowJobsQueryParams.java
new file mode 100644
index 00000000..109dbd65
--- /dev/null
+++ b/src/main/java/com/spotify/github/v3/actions/workflowjobs/ListWorkflowJobsQueryParams.java
@@ -0,0 +1,57 @@
+/*-
+ * -\-\-
+ * github-api
+ * --
+ * Copyright (C) 2016 - 2020 Spotify AB
+ * --
+ * Licensed 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.spotify.github.v3.actions.workflowjobs;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.spotify.github.GithubStyle;
+import com.spotify.github.Parameters;
+import org.immutables.value.Value;
+
+import java.util.Optional;
+
+@Value.Immutable
+@GithubStyle
+@JsonSerialize(as = ImmutableListWorkflowJobsQueryParams.class)
+@JsonDeserialize(as = ImmutableListWorkflowJobsQueryParams.class)
+public interface ListWorkflowJobsQueryParams extends Parameters {
+  Optional<Filter> filter();
+
+  /**
+   * The number of results per page (max 100). For more information, see "Using pagination in the REST API."
+   * &gt;p&lt;
+   * Default: 30
+   */
+  Optional<Integer> perPage();
+
+  /**
+   * The page number of the results to fetch. For more information, see "Using pagination in the REST API."
+   * &gt;p&lt;
+   * Default: 1
+   */
+  Optional<Integer> page();
+
+  enum Filter {
+    latest,
+    completed_at,
+    all
+  }
+}
diff --git a/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobConclusion.java b/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobConclusion.java
new file mode 100644
index 00000000..5c965190
--- /dev/null
+++ b/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobConclusion.java
@@ -0,0 +1,41 @@
+/*-
+ * -\-\-
+ * github-api
+ * --
+ * Copyright (C) 2016 - 2020 Spotify AB
+ * --
+ * Licensed 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.spotify.github.v3.actions.workflowjobs;
+
+/**
+ * The possible status values of a WorkflowJob's status field.
+ * &gt;p/&lt;
+ * Value of the status property can be one of: "queued", "in_progress", or "completed". Only GitHub Actions can set a status of "waiting", "pending", or "requested".
+ * When it’s “completed,” it makes sense to check if it finished successfully. We need a value of the conclusion property.
+ * Conclusion Can be one of the “success”, “failure”, “neutral”, “cancelled”, “skipped”, “timed_out”, or “action_required”.
+ * &gt;p/&lt;
+ * &#064;See <a href="https://docs.github.com/en/rest/actions/workflow-runs?apiVersion=2022-11-28#list-workflow-runs-for-a-repository">The GitHub API docs</a>
+ * &#064;See also <a href="https://github.com/github/rest-api-description/issues/1634#issuecomment-2230666873">GitHub rest api docs issue #1634</a>
+ */
+public enum WorkflowJobConclusion {
+  success,
+  failure,
+  neutral,
+  cancelled,
+  skipped,
+  timed_out,
+  action_required
+}
diff --git a/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobResponse.java b/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobResponse.java
new file mode 100644
index 00000000..26e81b14
--- /dev/null
+++ b/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobResponse.java
@@ -0,0 +1,168 @@
+/*-
+ * -\-\-
+ * github-api
+ * --
+ * Copyright (C) 2016 - 2020 Spotify AB
+ * --
+ * Licensed 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.spotify.github.v3.actions.workflowjobs;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.spotify.github.GithubStyle;
+import org.immutables.value.Value;
+
+import javax.annotation.Nullable;
+import java.time.ZonedDateTime;
+import java.util.List;
+
+@Value.Immutable
+@GithubStyle
+@JsonDeserialize(as = ImmutableWorkflowJobResponse.class)
+public interface WorkflowJobResponse {
+  /**
+   * The id of the job.
+   * (Required)
+   */
+  long id();
+
+  /**
+   * The id of the associated workflow run.
+   * (Required)
+   */
+  long runId();
+
+  /**
+   * (Required)
+   */
+  String runUrl();
+
+  /**
+   * Attempt number of the associated workflow run, 1 for first attempt and higher if the workflow was re-run.
+   */
+  @Nullable
+  Integer runAttempt();
+
+  /**
+   * (Required)
+   */
+  String nodeId();
+
+  /**
+   * The SHA of the commit that is being run.
+   * (Required)
+   */
+  String headSha();
+
+  /**
+   * (Required)
+   */
+  String url();
+
+  /**
+   * (Required)
+   */
+  String htmlUrl();
+
+  /**
+   * The phase of the lifecycle that the job is currently in.
+   * (Required)
+   */
+  WorkflowJobStatus status();
+
+  /**
+   * The outcome of the job.
+   * (Required)
+   */
+  WorkflowJobConclusion conclusion();
+
+  /**
+   * The time that the job created, in ISO 8601 format.
+   */
+  @Nullable
+  ZonedDateTime createdAt();
+
+  /**
+   * The time that the job started, in ISO 8601 format.
+   * (Required)
+   */
+  ZonedDateTime startedAt();
+
+  /**
+   * The time that the job finished, in ISO 8601 format.
+   * (Required)
+   */
+  ZonedDateTime completedAt();
+
+  /**
+   * The name of the job.
+   * (Required)
+   */
+  String name();
+
+  /**
+   * Steps in this job.
+   */
+  @Nullable
+  List<WorkflowJobStep> steps();
+
+  /**
+   * (Required)
+   */
+  String checkRunUrl();
+
+  /**
+   * Labels for the workflow job. Specified by the "runs_on" attribute in the action's workflow file.
+   * (Required)
+   */
+  List<String> labels();
+
+  /**
+   * The ID of the runner to which this job has been assigned. (If a runner hasn't yet been assigned, this will be null.)
+   * (Required)
+   */
+  int runnerId();
+
+  /**
+   * The name of the runner to which this job has been assigned. (If a runner hasn't yet been assigned, this will be null.)
+   * (Required)
+   */
+  String runnerName();
+
+  /**
+   * The ID of the runner group to which this job has been assigned. (If a runner hasn't yet been assigned, this will be null.)
+   * (Required)
+   */
+  int runnerGroupId();
+
+  /**
+   * The name of the runner group to which this job has been assigned. (If a runner hasn't yet been assigned, this will be null.)
+   * (Required)
+   */
+  String runnerGroupName();
+
+  /**
+   * The name of the workflow.
+   * (Required)
+   */
+  String workflowName();
+
+  /**
+   * The name of the current branch.
+   * (Required)
+   */
+  String headBranch();
+}
+
diff --git a/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobStatus.java b/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobStatus.java
new file mode 100644
index 00000000..97cf9c6e
--- /dev/null
+++ b/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobStatus.java
@@ -0,0 +1,40 @@
+/*-
+ * -\-\-
+ * github-api
+ * --
+ * Copyright (C) 2016 - 2020 Spotify AB
+ * --
+ * Licensed 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.spotify.github.v3.actions.workflowjobs;
+
+/**
+ * The possible status values of a WorkflowJob's status field.
+ * &gt;p/&lt;
+ * Value of the status property can be one of: "queued", "in_progress", or "completed". Only GitHub Actions can set a status of "waiting", "pending", or "requested".
+ * When it’s “completed,” it makes sense to check if it finished successfully. We need a value of the conclusion property.
+ * Conclusion Can be one of the “success”, “failure”, “neutral”, “cancelled”, “skipped”, “timed_out”, or “action_required”.
+ * &gt;p/&lt;
+ * &#064;See <a href="https://docs.github.com/en/rest/actions/workflow-runs?apiVersion=2022-11-28#list-workflow-runs-for-a-repository">The GitHub API docs</a>
+ * &#064;See also <a href="https://github.com/github/rest-api-description/issues/1634#issuecomment-2230666873">GitHub rest api docs issue #1634</a>
+ */
+public enum WorkflowJobStatus {
+  completed,
+  in_progress,
+  queued,
+  requested,
+  waiting,
+  pending
+}
diff --git a/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobStep.java b/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobStep.java
new file mode 100644
index 00000000..2c7f9f6c
--- /dev/null
+++ b/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobStep.java
@@ -0,0 +1,67 @@
+/*-
+ * -\-\-
+ * github-api
+ * --
+ * Copyright (C) 2016 - 2020 Spotify AB
+ * --
+ * Licensed 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.spotify.github.v3.actions.workflowjobs;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.spotify.github.GithubStyle;
+import org.immutables.value.Value;
+
+import javax.annotation.Nullable;
+import java.time.ZonedDateTime;
+
+@Value.Immutable
+@GithubStyle
+@JsonDeserialize(as = ImmutableWorkflowJobStep.class)
+public interface WorkflowJobStep {
+
+  /**
+   * The phase of the lifecycle that the job is currently in.
+   * (Required)
+   */
+  WorkflowJobStatus status();
+
+  /**
+   * The outcome of the job. Only set if status==completed
+   */
+  @Nullable
+  WorkflowJobConclusion conclusion();
+
+  /**
+   * The name of the job.
+   * (Required)
+   */
+  String name();
+
+  /**
+   * (Required)
+   */
+  Integer number();
+
+  /**
+   * The time that the step started, in ISO 8601 format.
+   */
+  ZonedDateTime startedAt();
+
+  /**
+   * The time that the job finished, in ISO 8601 format.
+   */
+  ZonedDateTime completedAt();
+}
diff --git a/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobsResponseList.java b/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobsResponseList.java
new file mode 100644
index 00000000..d4f3b917
--- /dev/null
+++ b/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobsResponseList.java
@@ -0,0 +1,51 @@
+/*-
+ * -\-\-
+ * github-api
+ * --
+ * Copyright (C) 2016 - 2020 Spotify AB
+ * --
+ * Licensed 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.spotify.github.v3.actions.workflowjobs;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.spotify.github.GithubStyle;
+import org.immutables.value.Value;
+
+import java.util.List;
+
+/**
+ * The Workflow Jobs list response
+ *
+ * @see "https://docs.github.com/en/rest/actions/workflow-jobs?apiVersion=2022-11-28"
+ */
+@Value.Immutable
+@GithubStyle
+@JsonDeserialize(as = ImmutableWorkflowJobsResponseList.class)
+public interface WorkflowJobsResponseList {
+  /**
+   * The count of Workflow Runs in the response
+   *
+   * @return the int
+   */
+  int totalCount();
+
+  /**
+   * Workflow runs list.
+   *
+   * @return the list of Workflow Runs
+   */
+  List<WorkflowJobResponse> jobs();
+}

From cf35a7e84450816b9dc7078c9802b787c12622bb Mon Sep 17 00:00:00 2001
From: Mitul Amin <mitul.github@shravi.dev>
Date: Wed, 22 Jan 2025 01:35:03 +0000
Subject: [PATCH 2/6] Added WorkflowJobsClient

The WorkflowJobsClient provides the following:
* listWorkflowJobs - get workflow jobs for a given workflow run
* getWorkflowJob - get a workflow job by ID

listWorkflowJobs support filtering via query parameters.
---
 .../github/v3/clients/ActionsClient.java      |  9 +++
 .../github/v3/clients/WorkflowJobsClient.java | 81 +++++++++++++++++++
 2 files changed, 90 insertions(+)
 create mode 100644 src/main/java/com/spotify/github/v3/clients/WorkflowJobsClient.java

diff --git a/src/main/java/com/spotify/github/v3/clients/ActionsClient.java b/src/main/java/com/spotify/github/v3/clients/ActionsClient.java
index 8dc25c86..ba3f7906 100644
--- a/src/main/java/com/spotify/github/v3/clients/ActionsClient.java
+++ b/src/main/java/com/spotify/github/v3/clients/ActionsClient.java
@@ -43,4 +43,13 @@ static ActionsClient create(final GitHubClient github, final String owner, final
   public WorkflowsClient createWorkflowsClient() {
     return WorkflowsClient.create(github, owner, repo);
   }
+
+  /**
+   * Workflow Jobs API client
+   *
+   * @return WorkflowJobs API client
+   */
+  public WorkflowJobsClient createWorkflowJobsClient() {
+    return WorkflowJobsClient.create(github, owner, repo);
+  }
 }
diff --git a/src/main/java/com/spotify/github/v3/clients/WorkflowJobsClient.java b/src/main/java/com/spotify/github/v3/clients/WorkflowJobsClient.java
new file mode 100644
index 00000000..9bd066a9
--- /dev/null
+++ b/src/main/java/com/spotify/github/v3/clients/WorkflowJobsClient.java
@@ -0,0 +1,81 @@
+/*-
+ * -\-\-
+ * github-api
+ * --
+ * Copyright (C) 2016 - 2020 Spotify AB
+ * --
+ * Licensed 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.spotify.github.v3.clients;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.spotify.github.v3.actions.workflowjobs.ListWorkflowJobsQueryParams;
+import com.spotify.github.v3.actions.workflowjobs.WorkflowJobResponse;
+import com.spotify.github.v3.actions.workflowjobs.WorkflowJobsResponseList;
+
+import javax.annotation.Nullable;
+import javax.ws.rs.core.HttpHeaders;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Workflow Runs API client
+ */
+public class WorkflowJobsClient {
+  private static final String LIST_WORKFLOW_RUN_JOBS_URI = "/repos/%s/%s/actions/runs/%s/jobs";
+  private static final String GET_WORKFLOW_JOB_URI = "/repos/%s/%s/actions/jobs/%s";
+
+  private final GitHubClient github;
+  private final String owner;
+  private final String repo;
+
+  private final Map<String, String> extraHeaders =
+      ImmutableMap.of(HttpHeaders.ACCEPT, "application/vnd.github+json");
+
+  public WorkflowJobsClient(final GitHubClient github, final String owner, final String repo) {
+    this.github = github;
+    this.owner = owner;
+    this.repo = repo;
+  }
+
+  static WorkflowJobsClient create(final GitHubClient github, final String owner, final String repo) {
+    return new WorkflowJobsClient(github, owner, repo);
+  }
+
+  /**
+   * List all workflow run jobs for a repository.
+   *
+   * @param queryParams optional parameters to add to the query. Can be null.
+   * @return a list of workflow run jobs for the repository
+   */
+  public CompletableFuture<WorkflowJobsResponseList> listWorkflowJobs(final long runId, @Nullable final ListWorkflowJobsQueryParams queryParams) {
+    final String serial = (queryParams == null ? "" : queryParams.serialize());
+    final String path = String.format(LIST_WORKFLOW_RUN_JOBS_URI, owner, repo, runId) + (Strings.isNullOrEmpty(serial) ? "" : "?" + serial);
+    return github.request(path, WorkflowJobsResponseList.class, extraHeaders);
+  }
+
+  /**
+   * Gets a workflow job by id.
+   *
+   * @param jobId the workflow job id to be retrieved
+   * @return a WorkflowRunResponse
+   */
+  public CompletableFuture<WorkflowJobResponse> getWorkflowJob(final long jobId, @Nullable final ListWorkflowJobsQueryParams queryParams) {
+    final String serial = (queryParams == null ? "" : queryParams.serialize());
+    final String path = String.format(GET_WORKFLOW_JOB_URI, owner, repo, jobId) + (Strings.isNullOrEmpty(serial) ? "" : "?" + serial);
+    return github.request(path, WorkflowJobResponse.class, extraHeaders);
+  }
+}

From 2307c493c8f32b7400d1d20fd7ec2400c0b6ede9 Mon Sep 17 00:00:00 2001
From: Mitul Amin <mitul.github@shravi.dev>
Date: Wed, 22 Jan 2025 01:37:26 +0000
Subject: [PATCH 3/6] Added WorkflowJobsClient tests

---
 .../v3/clients/WorkflowJobClientTest.java     | 142 ++++++++++++++++++
 ...workflowjobs-get-workflowjob-response.json | 108 +++++++++++++
 ...rkflowjobs-list-workflowjobs-response.json | 113 ++++++++++++++
 3 files changed, 363 insertions(+)
 create mode 100644 src/test/java/com/spotify/github/v3/clients/WorkflowJobClientTest.java
 create mode 100644 src/test/resources/com/spotify/github/v3/actions/workflowjobs/workflowjobs-get-workflowjob-response.json
 create mode 100644 src/test/resources/com/spotify/github/v3/actions/workflowjobs/workflowjobs-list-workflowjobs-response.json

diff --git a/src/test/java/com/spotify/github/v3/clients/WorkflowJobClientTest.java b/src/test/java/com/spotify/github/v3/clients/WorkflowJobClientTest.java
new file mode 100644
index 00000000..3c40b62c
--- /dev/null
+++ b/src/test/java/com/spotify/github/v3/clients/WorkflowJobClientTest.java
@@ -0,0 +1,142 @@
+/*-
+ * -\-\-
+ * github-api
+ * --
+ * Copyright (C) 2016 - 2020 Spotify AB
+ * --
+ * Licensed 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.spotify.github.v3.clients;
+
+import com.google.common.io.Resources;
+import com.spotify.github.jackson.Json;
+import com.spotify.github.v3.actions.workflowjobs.*;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.concurrent.CompletableFuture;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.core.Is.is;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class WorkflowJobClientTest {
+
+  private static final String FIXTURES_PATH = "com/spotify/github/v3/actions/workflowjobs/";
+  private GitHubClient github;
+  private WorkflowJobsClient workflowJobsClient;
+  private Json json;
+
+  public static String loadResource(final String path) {
+    try {
+      return Resources.toString(Resources.getResource(path), UTF_8);
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
+  }
+
+  @BeforeEach
+  public void setUp() {
+    github = mock(GitHubClient.class);
+    workflowJobsClient = new WorkflowJobsClient(github, "someowner", "somerepo");
+    json = Json.create();
+    when(github.json()).thenReturn(json);
+  }
+
+  @Test
+  public void getWorkflowRun() throws Exception {
+    final WorkflowJobResponse workflowJobResponse =
+        json.fromJson(
+            loadResource(FIXTURES_PATH + "workflowjobs-get-workflowjob-response.json"), WorkflowJobResponse.class);
+    final CompletableFuture<WorkflowJobResponse> fixtureResponse = completedFuture(workflowJobResponse);
+    when(github.request(any(), eq(WorkflowJobResponse.class), any())).thenReturn(fixtureResponse);
+
+    final CompletableFuture<WorkflowJobResponse> actualResponse =
+        workflowJobsClient.getWorkflowJob(29679449, null);
+
+    assertThat(actualResponse.get().id(), is(399444496L));
+    assertThat(actualResponse.get().status(), is(WorkflowJobStatus.completed));
+
+    assertThat(actualResponse.get().steps(), notNullValue());
+    assertThat(actualResponse.get().steps().size(), is(10));
+    assertThat(actualResponse.get().steps().get(0).name(), is("Set up job"));
+    assertThat(actualResponse.get().steps().get(1).name(), is("Run actions/checkout@v2"));
+  }
+
+  @Test
+  public void listWorkflowJobs() throws Exception {
+    final WorkflowJobsResponseList workflowJobsListResponse =
+        json.fromJson(
+            loadResource(FIXTURES_PATH + "workflowjobs-list-workflowjobs-response.json"), WorkflowJobsResponseList.class);
+    final CompletableFuture<WorkflowJobsResponseList> fixtureResponse = completedFuture(workflowJobsListResponse);
+    when(github.request(any(), eq(WorkflowJobsResponseList.class), any())).thenReturn(fixtureResponse);
+
+    final CompletableFuture<WorkflowJobsResponseList> actualResponse =
+        workflowJobsClient.listWorkflowJobs(159038, null);
+
+    assertThat(actualResponse.get().totalCount(), is(1));
+    assertThat(actualResponse.get().jobs().size(), is(1));
+
+    assertThat(actualResponse.get().jobs().get(0).id(), is(399444496L));
+    assertThat(actualResponse.get().jobs().get(0).status(), is(WorkflowJobStatus.completed));
+
+    assertThat(actualResponse.get().jobs().get(0).steps(), notNullValue());
+    assertThat(actualResponse.get().jobs().get(0).steps().size(), is(10));
+    assertThat(actualResponse.get().jobs().get(0).steps().get(0).name(), is("Set up job"));
+    assertThat(actualResponse.get().jobs().get(0).steps().get(1).name(), is("Run actions/checkout@v2"));
+  }
+
+
+  @Test
+  public void listWorkflowRunsFiltered() throws Exception {
+    ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
+
+    final WorkflowJobsResponseList workflowJobsListResponse =
+        json.fromJson(
+            loadResource(FIXTURES_PATH + "workflowjobs-list-workflowjobs-response.json"), WorkflowJobsResponseList.class);
+    final CompletableFuture<WorkflowJobsResponseList> fixtureResponse = completedFuture(workflowJobsListResponse);
+    when(github.request(pathCaptor.capture(), eq(WorkflowJobsResponseList.class), any())).thenReturn(fixtureResponse);
+
+    ListWorkflowJobsQueryParams params = ImmutableListWorkflowJobsQueryParams.builder()
+        .filter(ListWorkflowJobsQueryParams.Filter.latest)
+        .build();
+
+    final CompletableFuture<WorkflowJobsResponseList> actualResponse =
+        workflowJobsClient.listWorkflowJobs(159038, null);
+
+    assertThat(pathCaptor.getValue(), is("/repos/someowner/somerepo/actions/runs/159038/jobs"));
+
+    assertThat(actualResponse.get().totalCount(), is(1));
+    assertThat(actualResponse.get().jobs().size(), is(1));
+
+    assertThat(actualResponse.get().jobs().get(0).id(), is(399444496L));
+    assertThat(actualResponse.get().jobs().get(0).status(), is(WorkflowJobStatus.completed));
+
+    assertThat(actualResponse.get().jobs().get(0).steps(), notNullValue());
+    assertThat(actualResponse.get().jobs().get(0).steps().size(), is(10));
+    assertThat(actualResponse.get().jobs().get(0).steps().get(0).name(), is("Set up job"));
+    assertThat(actualResponse.get().jobs().get(0).steps().get(1).name(), is("Run actions/checkout@v2"));
+  }
+
+}
\ No newline at end of file
diff --git a/src/test/resources/com/spotify/github/v3/actions/workflowjobs/workflowjobs-get-workflowjob-response.json b/src/test/resources/com/spotify/github/v3/actions/workflowjobs/workflowjobs-get-workflowjob-response.json
new file mode 100644
index 00000000..1a51ee4a
--- /dev/null
+++ b/src/test/resources/com/spotify/github/v3/actions/workflowjobs/workflowjobs-get-workflowjob-response.json
@@ -0,0 +1,108 @@
+{
+  "id": 399444496,
+  "run_id": 29679449,
+  "run_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/29679449",
+  "node_id": "MDEyOldvcmtmbG93IEpvYjM5OTQ0NDQ5Ng==",
+  "head_sha": "f83a356604ae3c5d03e1b46ef4d1ca77d64a90b0",
+  "url": "https://api.github.com/repos/octo-org/octo-repo/actions/jobs/399444496",
+  "html_url": "https://github.com/octo-org/octo-repo/runs/29679449/jobs/399444496",
+  "status": "completed",
+  "conclusion": "success",
+  "started_at": "2020-01-20T17:42:40Z",
+  "completed_at": "2020-01-20T17:44:39Z",
+  "name": "build",
+  "steps": [
+    {
+      "name": "Set up job",
+      "status": "completed",
+      "conclusion": "success",
+      "number": 1,
+      "started_at": "2020-01-20T09:42:40.000-08:00",
+      "completed_at": "2020-01-20T09:42:41.000-08:00"
+    },
+    {
+      "name": "Run actions/checkout@v2",
+      "status": "completed",
+      "conclusion": "success",
+      "number": 2,
+      "started_at": "2020-01-20T09:42:41.000-08:00",
+      "completed_at": "2020-01-20T09:42:45.000-08:00"
+    },
+    {
+      "name": "Set up Ruby",
+      "status": "completed",
+      "conclusion": "success",
+      "number": 3,
+      "started_at": "2020-01-20T09:42:45.000-08:00",
+      "completed_at": "2020-01-20T09:42:45.000-08:00"
+    },
+    {
+      "name": "Run actions/cache@v3",
+      "status": "completed",
+      "conclusion": "success",
+      "number": 4,
+      "started_at": "2020-01-20T09:42:45.000-08:00",
+      "completed_at": "2020-01-20T09:42:48.000-08:00"
+    },
+    {
+      "name": "Install Bundler",
+      "status": "completed",
+      "conclusion": "success",
+      "number": 5,
+      "started_at": "2020-01-20T09:42:48.000-08:00",
+      "completed_at": "2020-01-20T09:42:52.000-08:00"
+    },
+    {
+      "name": "Install Gems",
+      "status": "completed",
+      "conclusion": "success",
+      "number": 6,
+      "started_at": "2020-01-20T09:42:52.000-08:00",
+      "completed_at": "2020-01-20T09:42:53.000-08:00"
+    },
+    {
+      "name": "Run Tests",
+      "status": "completed",
+      "conclusion": "success",
+      "number": 7,
+      "started_at": "2020-01-20T09:42:53.000-08:00",
+      "completed_at": "2020-01-20T09:42:59.000-08:00"
+    },
+    {
+      "name": "Deploy to Heroku",
+      "status": "completed",
+      "conclusion": "success",
+      "number": 8,
+      "started_at": "2020-01-20T09:42:59.000-08:00",
+      "completed_at": "2020-01-20T09:44:39.000-08:00"
+    },
+    {
+      "name": "Post actions/cache@v3",
+      "status": "completed",
+      "conclusion": "success",
+      "number": 16,
+      "started_at": "2020-01-20T09:44:39.000-08:00",
+      "completed_at": "2020-01-20T09:44:39.000-08:00"
+    },
+    {
+      "name": "Complete job",
+      "status": "completed",
+      "conclusion": "success",
+      "number": 17,
+      "started_at": "2020-01-20T09:44:39.000-08:00",
+      "completed_at": "2020-01-20T09:44:39.000-08:00"
+    }
+  ],
+  "check_run_url": "https://api.github.com/repos/octo-org/octo-repo/check-runs/399444496",
+  "labels": [
+    "self-hosted",
+    "foo",
+    "bar"
+  ],
+  "runner_id": 1,
+  "runner_name": "my runner",
+  "runner_group_id": 2,
+  "runner_group_name": "my runner group",
+  "workflow_name": "CI",
+  "head_branch": "main"
+}
\ No newline at end of file
diff --git a/src/test/resources/com/spotify/github/v3/actions/workflowjobs/workflowjobs-list-workflowjobs-response.json b/src/test/resources/com/spotify/github/v3/actions/workflowjobs/workflowjobs-list-workflowjobs-response.json
new file mode 100644
index 00000000..bbff361f
--- /dev/null
+++ b/src/test/resources/com/spotify/github/v3/actions/workflowjobs/workflowjobs-list-workflowjobs-response.json
@@ -0,0 +1,113 @@
+{
+  "total_count": 1,
+  "jobs": [
+    {
+      "id": 399444496,
+      "run_id": 29679449,
+      "run_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/29679449",
+      "node_id": "MDEyOldvcmtmbG93IEpvYjM5OTQ0NDQ5Ng==",
+      "head_sha": "f83a356604ae3c5d03e1b46ef4d1ca77d64a90b0",
+      "url": "https://api.github.com/repos/octo-org/octo-repo/actions/jobs/399444496",
+      "html_url": "https://github.com/octo-org/octo-repo/runs/29679449/jobs/399444496",
+      "status": "completed",
+      "conclusion": "success",
+      "started_at": "2020-01-20T17:42:40Z",
+      "completed_at": "2020-01-20T17:44:39Z",
+      "name": "build",
+      "steps": [
+        {
+          "name": "Set up job",
+          "status": "completed",
+          "conclusion": "success",
+          "number": 1,
+          "started_at": "2020-01-20T09:42:40.000-08:00",
+          "completed_at": "2020-01-20T09:42:41.000-08:00"
+        },
+        {
+          "name": "Run actions/checkout@v2",
+          "status": "completed",
+          "conclusion": "success",
+          "number": 2,
+          "started_at": "2020-01-20T09:42:41.000-08:00",
+          "completed_at": "2020-01-20T09:42:45.000-08:00"
+        },
+        {
+          "name": "Set up Ruby",
+          "status": "completed",
+          "conclusion": "success",
+          "number": 3,
+          "started_at": "2020-01-20T09:42:45.000-08:00",
+          "completed_at": "2020-01-20T09:42:45.000-08:00"
+        },
+        {
+          "name": "Run actions/cache@v3",
+          "status": "completed",
+          "conclusion": "success",
+          "number": 4,
+          "started_at": "2020-01-20T09:42:45.000-08:00",
+          "completed_at": "2020-01-20T09:42:48.000-08:00"
+        },
+        {
+          "name": "Install Bundler",
+          "status": "completed",
+          "conclusion": "success",
+          "number": 5,
+          "started_at": "2020-01-20T09:42:48.000-08:00",
+          "completed_at": "2020-01-20T09:42:52.000-08:00"
+        },
+        {
+          "name": "Install Gems",
+          "status": "completed",
+          "conclusion": "success",
+          "number": 6,
+          "started_at": "2020-01-20T09:42:52.000-08:00",
+          "completed_at": "2020-01-20T09:42:53.000-08:00"
+        },
+        {
+          "name": "Run Tests",
+          "status": "completed",
+          "conclusion": "success",
+          "number": 7,
+          "started_at": "2020-01-20T09:42:53.000-08:00",
+          "completed_at": "2020-01-20T09:42:59.000-08:00"
+        },
+        {
+          "name": "Deploy to Heroku",
+          "status": "completed",
+          "conclusion": "success",
+          "number": 8,
+          "started_at": "2020-01-20T09:42:59.000-08:00",
+          "completed_at": "2020-01-20T09:44:39.000-08:00"
+        },
+        {
+          "name": "Post actions/cache@v3",
+          "status": "completed",
+          "conclusion": "success",
+          "number": 16,
+          "started_at": "2020-01-20T09:44:39.000-08:00",
+          "completed_at": "2020-01-20T09:44:39.000-08:00"
+        },
+        {
+          "name": "Complete job",
+          "status": "completed",
+          "conclusion": "success",
+          "number": 17,
+          "started_at": "2020-01-20T09:44:39.000-08:00",
+          "completed_at": "2020-01-20T09:44:39.000-08:00"
+        }
+      ],
+      "check_run_url": "https://api.github.com/repos/octo-org/octo-repo/check-runs/399444496",
+      "labels": [
+        "self-hosted",
+        "foo",
+        "bar"
+      ],
+      "runner_id": 1,
+      "runner_name": "my runner",
+      "runner_group_id": 2,
+      "runner_group_name": "my runner group",
+      "workflow_name": "CI",
+      "head_branch": "main"
+    }
+  ]
+}
\ No newline at end of file

From 6353a683cae6db3459fc5ec2150b9ef98bbfdb0a Mon Sep 17 00:00:00 2001
From: Mitul Amin <mitul.github@shravi.dev>
Date: Wed, 22 Jan 2025 15:49:26 +0000
Subject: [PATCH 4/6] Fixed WorkflowJobResponse.runnerId to be a Nullable
 Integer

Found a case where GitHub returns a null for this field.
---
 .../github/v3/actions/workflowjobs/WorkflowJobResponse.java   | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobResponse.java b/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobResponse.java
index 26e81b14..87bb2561 100644
--- a/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobResponse.java
+++ b/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobResponse.java
@@ -131,9 +131,9 @@ public interface WorkflowJobResponse {
 
   /**
    * The ID of the runner to which this job has been assigned. (If a runner hasn't yet been assigned, this will be null.)
-   * (Required)
    */
-  int runnerId();
+  @Nullable
+  Integer runnerId();
 
   /**
    * The name of the runner to which this job has been assigned. (If a runner hasn't yet been assigned, this will be null.)

From 83c202a2d3d5ff2a4ea575712e05b757cb0925ea Mon Sep 17 00:00:00 2001
From: Mitul Amin <mitul.github@shravi.dev>
Date: Wed, 22 Jan 2025 15:54:22 +0000
Subject: [PATCH 5/6] Fixed WorkflowJobResponse.runnerGroupId to be a Nullable
 Integer

Found a case where GitHub returns a null for this field.
---
 .../github/v3/actions/workflowjobs/WorkflowJobResponse.java   | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobResponse.java b/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobResponse.java
index 87bb2561..1183955b 100644
--- a/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobResponse.java
+++ b/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobResponse.java
@@ -143,9 +143,9 @@ public interface WorkflowJobResponse {
 
   /**
    * The ID of the runner group to which this job has been assigned. (If a runner hasn't yet been assigned, this will be null.)
-   * (Required)
    */
-  int runnerGroupId();
+  @Nullable
+  Integer runnerGroupId();
 
   /**
    * The name of the runner group to which this job has been assigned. (If a runner hasn't yet been assigned, this will be null.)

From 1d49e6255536252cccbf0c8853d4d3bea24df681 Mon Sep 17 00:00:00 2001
From: Mitul Amin <mitul.github@shravi.dev>
Date: Wed, 22 Jan 2025 15:57:04 +0000
Subject: [PATCH 6/6] WorkflowJobResponse's runnerName and runnerGroupName are
 Nullable

Found a case where GitHub returns a null for these field.
---
 .../github/v3/actions/workflowjobs/WorkflowJobResponse.java | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobResponse.java b/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobResponse.java
index 1183955b..2a7f8f48 100644
--- a/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobResponse.java
+++ b/src/main/java/com/spotify/github/v3/actions/workflowjobs/WorkflowJobResponse.java
@@ -137,8 +137,8 @@ public interface WorkflowJobResponse {
 
   /**
    * The name of the runner to which this job has been assigned. (If a runner hasn't yet been assigned, this will be null.)
-   * (Required)
    */
+  @Nullable
   String runnerName();
 
   /**
@@ -148,9 +148,9 @@ public interface WorkflowJobResponse {
   Integer runnerGroupId();
 
   /**
-   * The name of the runner group to which this job has been assigned. (If a runner hasn't yet been assigned, this will be null.)
-   * (Required)
+   * The name of the runner group to which this job has been assigned. (If a runner hasn't yet been assigned, this will be null.)* (Required)
    */
+  @Nullable
   String runnerGroupName();
 
   /**