Repository: hub4j/github-api (java)

Issue:
Implement workflow job event payload
**Describe the bug**
#1316 added `WORKFLOW_JOB` to the GHEvent enum, but receiving workflow_job events is still not fully supported because it can’t be mapped to any GHEventPayload subclass.  An attempt to map it to the GHEventPayload superclass results in:
> com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.kohsuke.github.GHEventPayload` (no Creators, like default constructor, exist)

I’ve captured the request body for a workflow_job event in order to check its structure, and it looks like implementing this is very straightforward.  I used the WorkflowRun subclass as a model.
```
diff --git a/src/main/java/org/kohsuke/github/GHEventPayload.java b/src/main/java/org/kohsuke/github/GHEventPayload.java
index 4d47264aa..ca3b75036 100644
--- a/src/main/java/org/kohsuke/github/GHEventPayload.java
+++ b/src/main/java/org/kohsuke/github/GHEventPayload.java
@@ -1446,6 +1446,42 @@ public abstract class GHEventPayload extends GitHubInteractiveObject {
         }
     }
 
+    /**
+     * A workflow job has been queued, is in progress, or has been completed.
+     *
+     * @see <a href=
+     *      "https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#workflow_job">
+     *      workflow job event</a>
+     * @see <a href="https://docs.github.com/en/rest/reference/actions#workflow-jobs">Actions Workflow Jobs</a>
+     */
+    public static class WorkflowJob extends GHEventPayload {
+        private GHWorkflowJob workflowJob;
+
+        /**
+         * Gets the workflow job.
+         *
+         * @return the workflow job
+         */
+        @SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Expected")
+        public GHWorkflowJob getWorkflowJob() {
+            return workflowJob;
+        }
+
+        @Override
+        void lateBind() {
+            if (workflowJob == null) {
+                throw new IllegalStateException(
+                        "Expected workflow_job payload, but got something else.  Maybe we've got another type of event?");
+            }
+            super.lateBind();
+            GHRepository repository = getRepository();
+            if (repository == null) {
+                throw new IllegalStateException("Repository must not be null");
+            }
+            workflowJob.wrapUp(repository);
+        }
+    }
+
     /**
      * A label was created, edited or deleted.
      *
```

I’ve tested this locally by posting a copy of the workflow_job event to my application’s webhook endpoint, where the application is doing:
```
        Class<? extends GHEventPayload> eventClass;
        switch (eventName.toLowerCase()) {

            …

            case "workflow_job":
                eventClass = GHEventPayload.WorkflowJob.class; break;

            default:
                throw new UnsupportedOperationException(
                        "Event \"" + eventName + "\" is not handled");

        }

        GHEventPayload eventPayload = GitHub.offline().parseEventPayload(
                new StringReader(payload), eventClass);
```


Pull request description:
Add support for workflow_job event payload
Fixes #1331

Patch:
diff --git a/src/main/java/org/kohsuke/github/GHEventPayload.java b/src/main/java/org/kohsuke/github/GHEventPayload.java
index 0f1728e10..e7af6e93d 100644
--- a/src/main/java/org/kohsuke/github/GHEventPayload.java
+++ b/src/main/java/org/kohsuke/github/GHEventPayload.java
@@ -1506,6 +1506,43 @@ public abstract class GHEventPayload extends GitHubInteractiveObject {
         }
     }
 
+    /**
+     * A workflow job has been queued, is in progress, or has been completed.
+     *
+     * @see <a href=
+     *      "https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#workflow_job">
+     *      workflow job event</a>
+     * @see <a href="https://docs.github.com/en/rest/reference/actions#workflow-jobs">Actions Workflow Jobs</a>
+     */
+    public static class WorkflowJob extends GHEventPayload {
+
+        private GHWorkflowJob workflowJob;
+
+        /**
+         * Gets the workflow job.
+         *
+         * @return the workflow job
+         */
+        @SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Expected")
+        public GHWorkflowJob getWorkflowJob() {
+            return workflowJob;
+        }
+
+        @Override
+        void lateBind() {
+            if (workflowJob == null) {
+                throw new IllegalStateException(
+                        "Expected workflow_job payload, but got something else.  Maybe we've got another type of event?");
+            }
+            super.lateBind();
+            GHRepository repository = getRepository();
+            if (repository == null) {
+                throw new IllegalStateException("Repository must not be null");
+            }
+            workflowJob.wrapUp(repository);
+        }
+    }
+
     /**
      * A label was created, edited or deleted.
      *
diff --git a/src/main/java/org/kohsuke/github/GHWorkflowJob.java b/src/main/java/org/kohsuke/github/GHWorkflowJob.java
index 9e701d7f3..767075550 100644
--- a/src/main/java/org/kohsuke/github/GHWorkflowJob.java
+++ b/src/main/java/org/kohsuke/github/GHWorkflowJob.java
@@ -35,6 +35,7 @@ public class GHWorkflowJob extends GHObject {
     private String conclusion;
 
     private long runId;
+    private int runAttempt;
 
     private String htmlUrl;
     private String checkRunUrl;
@@ -108,6 +109,15 @@ public class GHWorkflowJob extends GHObject {
         return runId;
     }
 
+    /**
+     * Attempt number of the associated workflow run, 1 for first attempt and higher if the workflow was re-run.
+     *
+     * @return attempt number
+     */
+    public int getRunAttempt() {
+        return runAttempt;
+    }
+
     @Override
     public URL getHtmlUrl() {
         return GitHubClient.parseURL(htmlUrl);


Test patch:
diff --git a/src/test/java/org/kohsuke/github/GHEventPayloadTest.java b/src/test/java/org/kohsuke/github/GHEventPayloadTest.java
index 93616b008..0c07d1efe 100644
--- a/src/test/java/org/kohsuke/github/GHEventPayloadTest.java
+++ b/src/test/java/org/kohsuke/github/GHEventPayloadTest.java
@@ -11,7 +11,18 @@ import java.util.Collections;
 import java.util.List;
 import java.util.TimeZone;
 
-import static org.hamcrest.Matchers.*;
+import static org.hamcrest.Matchers.aMapWithSize;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.endsWith;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasProperty;
+import static org.hamcrest.Matchers.hasToString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.hamcrest.Matchers.startsWith;
 import static org.junit.Assert.assertThrows;
 
 public class GHEventPayloadTest extends AbstractGitHubWireMockTest {
@@ -876,7 +887,6 @@ public class GHEventPayloadTest extends AbstractGitHubWireMockTest {
         GHPullRequest pullRequest = pullRequests.get(0);
         assertThat(pullRequest.getId(), is(599098265L));
         assertThat(pullRequest.getRepository(), sameInstance(workflowRunPayload.getRepository()));
-
     }
 
     @Test
@@ -892,6 +902,40 @@ public class GHEventPayloadTest extends AbstractGitHubWireMockTest {
         assertThat(workflowRunPayload.getWorkflow().getRepository(), sameInstance(workflowRunPayload.getRepository()));
     }
 
+    @Test
+    public void workflow_job() throws Exception {
+        final GHEventPayload.WorkflowJob workflowJobPayload = GitHub.offline()
+                .parseEventPayload(payload.asReader(), GHEventPayload.WorkflowJob.class);
+
+        assertThat(workflowJobPayload.getAction(), is("completed"));
+        assertThat(workflowJobPayload.getRepository().getFullName(), is("gsmet/quarkus-bot-java-playground"));
+        assertThat(workflowJobPayload.getSender().getLogin(), is("gsmet"));
+
+        GHWorkflowJob workflowJob = workflowJobPayload.getWorkflowJob();
+        assertThat(workflowJob.getId(), is(6653410527L));
+        assertThat(workflowJob.getRunId(), is(2408553341L));
+        assertThat(workflowJob.getRunAttempt(), is(1));
+        assertThat(workflowJob.getUrl().toString(),
+                is("https://api.github.com/repos/gsmet/quarkus-bot-java-playground/actions/jobs/6653410527"));
+        assertThat(workflowJob.getHtmlUrl().toString(),
+                is("https://github.com/gsmet/quarkus-bot-java-playground/runs/6653410527?check_suite_focus=true"));
+        assertThat(workflowJob.getNodeId(), is("CR_kwDOEq3cwc8AAAABjJL83w"));
+        assertThat(workflowJob.getHeadSha(), is("5dd2dadfbdc2a722c08a8ad42ae4e26e3e731042"));
+        assertThat(workflowJob.getStatus(), is(GHWorkflowRun.Status.COMPLETED));
+        assertThat(workflowJob.getConclusion(), is(GHWorkflowRun.Conclusion.FAILURE));
+        assertThat(workflowJob.getStartedAt().getTime(), is(1653908125000L));
+        assertThat(workflowJob.getCompletedAt().getTime(), is(1653908157000L));
+        assertThat(workflowJob.getName(), is("JVM Tests - JDK JDK16"));
+        assertThat(workflowJob.getSteps(),
+                contains(hasProperty("name", is("Set up job")),
+                        hasProperty("name", is("Run actions/checkout@v2")),
+                        hasProperty("name", is("Build with Maven")),
+                        hasProperty("name", is("Post Run actions/checkout@v2")),
+                        hasProperty("name", is("Complete job"))));
+        assertThat(workflowJob.getCheckRunUrl().toString(),
+                is("https://api.github.com/repos/gsmet/quarkus-bot-java-playground/check-runs/6653410527"));
+    }
+
     @Test
     public void label_created() throws Exception {
         final GHEventPayload.Label labelPayload = GitHub.offline()
diff --git a/src/test/resources/org/kohsuke/github/GHEventPayloadTest/workflow_job.json b/src/test/resources/org/kohsuke/github/GHEventPayloadTest/workflow_job.json
new file mode 100644
index 000000000..b51176510
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/GHEventPayloadTest/workflow_job.json
@@ -0,0 +1,190 @@
+{
+  "action": "completed",
+  "workflow_job": {
+    "id": 6653410527,
+    "run_id": 2408553341,
+    "run_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/actions/runs/2408553341",
+    "run_attempt": 1,
+    "node_id": "CR_kwDOEq3cwc8AAAABjJL83w",
+    "head_sha": "5dd2dadfbdc2a722c08a8ad42ae4e26e3e731042",
+    "url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/actions/jobs/6653410527",
+    "html_url": "https://github.com/gsmet/quarkus-bot-java-playground/runs/6653410527?check_suite_focus=true",
+    "status": "completed",
+    "conclusion": "failure",
+    "started_at": "2022-05-30T10:55:25Z",
+    "completed_at": "2022-05-30T10:55:57Z",
+    "name": "JVM Tests - JDK JDK16",
+    "steps": [
+      {
+        "name": "Set up job",
+        "status": "completed",
+        "conclusion": "success",
+        "number": 1,
+        "started_at": "2022-05-30T10:55:25.000Z",
+        "completed_at": "2022-05-30T10:55:27.000Z"
+      },
+      {
+        "name": "Run actions/checkout@v2",
+        "status": "completed",
+        "conclusion": "success",
+        "number": 2,
+        "started_at": "2022-05-30T10:55:27.000Z",
+        "completed_at": "2022-05-30T10:55:27.000Z"
+      },
+      {
+        "name": "Build with Maven",
+        "status": "completed",
+        "conclusion": "failure",
+        "number": 4,
+        "started_at": "2022-05-30T10:55:35.000Z",
+        "completed_at": "2022-05-30T10:55:55.000Z"
+      },
+      {
+        "name": "Post Run actions/checkout@v2",
+        "status": "completed",
+        "conclusion": "success",
+        "number": 10,
+        "started_at": "2022-05-30T10:55:55.000Z",
+        "completed_at": "2022-05-30T10:55:56.000Z"
+      },
+      {
+        "name": "Complete job",
+        "status": "completed",
+        "conclusion": "success",
+        "number": 11,
+        "started_at": "2022-05-30T10:55:56.000Z",
+        "completed_at": "2022-05-30T10:55:56.000Z"
+      }
+    ],
+    "check_run_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/check-runs/6653410527",
+    "labels": [
+      "ubuntu-latest"
+    ],
+    "runner_id": 10,
+    "runner_name": "GitHub Actions 10",
+    "runner_group_id": 2,
+    "runner_group_name": "GitHub Actions"
+  },
+  "repository": {
+    "id": 313384129,
+    "node_id": "MDEwOlJlcG9zaXRvcnkzMTMzODQxMjk=",
+    "name": "quarkus-bot-java-playground",
+    "full_name": "gsmet/quarkus-bot-java-playground",
+    "private": false,
+    "owner": {
+      "login": "gsmet",
+      "id": 1279749,
+      "node_id": "MDQ6VXNlcjEyNzk3NDk=",
+      "avatar_url": "https://avatars.githubusercontent.com/u/1279749?v=4",
+      "gravatar_id": "",
+      "url": "https://api.github.com/users/gsmet",
+      "html_url": "https://github.com/gsmet",
+      "followers_url": "https://api.github.com/users/gsmet/followers",
+      "following_url": "https://api.github.com/users/gsmet/following{/other_user}",
+      "gists_url": "https://api.github.com/users/gsmet/gists{/gist_id}",
+      "starred_url": "https://api.github.com/users/gsmet/starred{/owner}{/repo}",
+      "subscriptions_url": "https://api.github.com/users/gsmet/subscriptions",
+      "organizations_url": "https://api.github.com/users/gsmet/orgs",
+      "repos_url": "https://api.github.com/users/gsmet/repos",
+      "events_url": "https://api.github.com/users/gsmet/events{/privacy}",
+      "received_events_url": "https://api.github.com/users/gsmet/received_events",
+      "type": "User",
+      "site_admin": false
+    },
+    "html_url": "https://github.com/gsmet/quarkus-bot-java-playground",
+    "description": null,
+    "fork": false,
+    "url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground",
+    "forks_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/forks",
+    "keys_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/keys{/key_id}",
+    "collaborators_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/collaborators{/collaborator}",
+    "teams_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/teams",
+    "hooks_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/hooks",
+    "issue_events_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/issues/events{/number}",
+    "events_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/events",
+    "assignees_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/assignees{/user}",
+    "branches_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/branches{/branch}",
+    "tags_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/tags",
+    "blobs_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/git/blobs{/sha}",
+    "git_tags_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/git/tags{/sha}",
+    "git_refs_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/git/refs{/sha}",
+    "trees_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/git/trees{/sha}",
+    "statuses_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/statuses/{sha}",
+    "languages_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/languages",
+    "stargazers_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/stargazers",
+    "contributors_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/contributors",
+    "subscribers_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/subscribers",
+    "subscription_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/subscription",
+    "commits_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/commits{/sha}",
+    "git_commits_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/git/commits{/sha}",
+    "comments_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/comments{/number}",
+    "issue_comment_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/issues/comments{/number}",
+    "contents_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/contents/{+path}",
+    "compare_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/compare/{base}...{head}",
+    "merges_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/merges",
+    "archive_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/{archive_format}{/ref}",
+    "downloads_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/downloads",
+    "issues_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/issues{/number}",
+    "pulls_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/pulls{/number}",
+    "milestones_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/milestones{/number}",
+    "notifications_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/notifications{?since,all,participating}",
+    "labels_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/labels{/name}",
+    "releases_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/releases{/id}",
+    "deployments_url": "https://api.github.com/repos/gsmet/quarkus-bot-java-playground/deployments",
+    "created_at": "2020-11-16T17:55:53Z",
+    "updated_at": "2021-12-22T17:20:52Z",
+    "pushed_at": "2022-05-30T10:55:11Z",
+    "git_url": "git://github.com/gsmet/quarkus-bot-java-playground.git",
+    "ssh_url": "git@github.com:gsmet/quarkus-bot-java-playground.git",
+    "clone_url": "https://github.com/gsmet/quarkus-bot-java-playground.git",
+    "svn_url": "https://github.com/gsmet/quarkus-bot-java-playground",
+    "homepage": null,
+    "size": 88,
+    "stargazers_count": 0,
+    "watchers_count": 0,
+    "language": "Java",
+    "has_issues": true,
+    "has_projects": true,
+    "has_downloads": true,
+    "has_wiki": true,
+    "has_pages": false,
+    "forks_count": 2,
+    "mirror_url": null,
+    "archived": false,
+    "disabled": false,
+    "open_issues_count": 36,
+    "license": null,
+    "allow_forking": true,
+    "is_template": false,
+    "topics": [],
+    "visibility": "public",
+    "forks": 2,
+    "open_issues": 36,
+    "watchers": 0,
+    "default_branch": "main"
+  },
+  "sender": {
+    "login": "gsmet",
+    "id": 1279749,
+    "node_id": "MDQ6VXNlcjEyNzk3NDk=",
+    "avatar_url": "https://avatars.githubusercontent.com/u/1279749?v=4",
+    "gravatar_id": "",
+    "url": "https://api.github.com/users/gsmet",
+    "html_url": "https://github.com/gsmet",
+    "followers_url": "https://api.github.com/users/gsmet/followers",
+    "following_url": "https://api.github.com/users/gsmet/following{/other_user}",
+    "gists_url": "https://api.github.com/users/gsmet/gists{/gist_id}",
+    "starred_url": "https://api.github.com/users/gsmet/starred{/owner}{/repo}",
+    "subscriptions_url": "https://api.github.com/users/gsmet/subscriptions",
+    "organizations_url": "https://api.github.com/users/gsmet/orgs",
+    "repos_url": "https://api.github.com/users/gsmet/repos",
+    "events_url": "https://api.github.com/users/gsmet/events{/privacy}",
+    "received_events_url": "https://api.github.com/users/gsmet/received_events",
+    "type": "User",
+    "site_admin": false
+  },
+  "installation": {
+    "id": 13005535,
+    "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMTMwMDU1MzU="
+  }
+}
\ No newline at end of file