{"action":"created","comment":{"url":"https://api.github.com/repos/gocd/gocd/pulls/comments/51088543","id":51088543,"diff_hunk":"@@ -0,0 +1,212 @@\n+/*\n+ * Copyright 2015 ThoughtWorks, Inc.\n+ *\n+ * Licensed under the Apache License, Version 2.0 (the \"License\");\n+ * you may not use this file except in compliance with the License.\n+ * You may obtain a copy of the License at\n+ *\n+ *     http://www.apache.org/licenses/LICENSE-2.0\n+ *\n+ * Unless required by applicable law or agreed to in writing, software\n+ * distributed under the License is distributed on an \"AS IS\" BASIS,\n+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+ * See the License for the specific language governing permissions and\n+ * limitations under the License.\n+ */\n+\n+package com.thoughtworks.go.agent.service;\n+\n+import com.thoughtworks.go.agent.AgentController;\n+import com.thoughtworks.go.agent.JobRunner;\n+import com.thoughtworks.go.domain.JobIdentifier;\n+import com.thoughtworks.go.domain.JobResult;\n+import com.thoughtworks.go.domain.JobState;\n+import com.thoughtworks.go.remote.AgentIdentifier;\n+import com.thoughtworks.go.remote.AgentInstruction;\n+import com.thoughtworks.go.remote.BuildRepositoryRemote;\n+import com.thoughtworks.go.remote.work.Work;\n+import com.thoughtworks.go.server.service.AgentRuntimeInfo;\n+import com.thoughtworks.go.server.websocket.Action;\n+import com.thoughtworks.go.server.websocket.Message;\n+import com.thoughtworks.go.server.websocket.Report;\n+import com.thoughtworks.go.util.SystemEnvironment;\n+import com.thoughtworks.go.util.URLService;\n+import org.eclipse.jetty.util.ssl.SslContextFactory;\n+import org.eclipse.jetty.websocket.api.MessageTooLargeException;\n+import org.eclipse.jetty.websocket.api.Session;\n+import org.eclipse.jetty.websocket.api.annotations.*;\n+import org.eclipse.jetty.websocket.api.extensions.Frame;\n+import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;\n+import org.eclipse.jetty.websocket.client.WebSocketClient;\n+import org.slf4j.Logger;\n+import org.slf4j.LoggerFactory;\n+import org.springframework.beans.factory.annotation.Autowired;\n+import org.springframework.stereotype.Component;\n+\n+import java.io.IOException;\n+import java.io.InputStream;\n+import java.net.URI;\n+import java.nio.ByteBuffer;\n+import java.util.concurrent.Executor;\n+import java.util.concurrent.Executors;\n+\n+@Component\n+@WebSocket\n+public class AgentWebsocketService {\n+    public static class BuildRepositoryRemoteAdapter implements BuildRepositoryRemote {\n+        private JobRunner runner;\n+        private AgentWebsocketService service;\n+\n+        public BuildRepositoryRemoteAdapter(JobRunner runner, AgentWebsocketService service) {\n+            this.runner = runner;\n+            this.service = service;\n+        }\n+        @Override\n+        public AgentInstruction ping(AgentRuntimeInfo info) {\n+            throw new UnsupportedOperationException();\n+        }\n+\n+        @Override\n+        public Work getWork(AgentRuntimeInfo runtimeInfo) {\n+            throw new UnsupportedOperationException();\n+        }\n+\n+        @Override\n+        public void reportCurrentStatus(AgentRuntimeInfo agentRuntimeInfo, JobIdentifier jobIdentifier, JobState jobState) {\n+            service.send(new Message(Action.reportCurrentStatus, new Report(agentRuntimeInfo, jobIdentifier, jobState)));\n+        }\n+\n+        @Override\n+        public void reportCompleting(AgentRuntimeInfo agentRuntimeInfo, JobIdentifier jobIdentifier, JobResult result) {\n+            service.send(new Message(Action.reportCompleting, new Report(agentRuntimeInfo, jobIdentifier, result)));\n+        }\n+\n+        @Override\n+        public void reportCompleted(AgentRuntimeInfo agentRuntimeInfo, JobIdentifier jobIdentifier, JobResult result) {\n+            service.send(new Message(Action.reportCompleted, new Report(agentRuntimeInfo, jobIdentifier, result)));\n+        }\n+\n+        @Override\n+        public boolean isIgnored(JobIdentifier jobIdentifier) {\n+            return runner.isJobCancelled();\n+        }\n+\n+        @Override\n+        public String getCookie(AgentIdentifier identifier, String location) {\n+            throw new UnsupportedOperationException();\n+        }\n+    }\n+\n+    private static final Logger LOGGER = LoggerFactory.getLogger(AgentWebsocketService.class);\n+    private AgentController controller;\n+    private Session session;\n+    private URLService urlService;\n+    private WebSocketClient client;\n+    private Executor executor = Executors.newFixedThreadPool(5);\n+\n+    @Autowired\n+    public AgentWebsocketService(URLService urlService) {\n+        this.urlService = urlService;\n+    }\n+\n+    public synchronized void start() throws Exception {\n+        SslContextFactory sslContextFactory = new SslContextFactory();\n+        sslContextFactory.setKeyStorePath(SslInfrastructureService.AGENT_CERTIFICATE_FILE.getAbsolutePath());\n+        sslContextFactory.setKeyStorePassword(SslInfrastructureService.AGENT_STORE_PASSWORD);\n+        sslContextFactory.setKeyManagerPassword(SslInfrastructureService.AGENT_STORE_PASSWORD);\n+        sslContextFactory.setTrustStorePath(SslInfrastructureService.AGENT_TRUST_FILE.getAbsolutePath());\n+        sslContextFactory.setTrustStorePassword(SslInfrastructureService.AGENT_STORE_PASSWORD);\n+        sslContextFactory.setWantClientAuth(true);\n+        if (client == null || client.isStopped()) {\n+            SystemEnvironment environment = new SystemEnvironment();\n+            client = new WebSocketClient(sslContextFactory);\n+            client.setMaxIdleTimeout(environment.getWebsocketMaxIdleTime());\n+            Message.setupPolicy(client.getPolicy(), environment);\n+            client.start();\n+        }\n+        if (session != null) {\n+            session.close();\n+        }\n+        LOGGER.info(\"Connecting to websocket endpoint: \" + urlService.getAgentRemoteWebSocketUrl());\n+        session = client.connect(this, new URI(urlService.getAgentRemoteWebSocketUrl()), new ClientUpgradeRequest()).get();\n+    }\n+\n+    public synchronized void stop() {\n+        if (isRunning()) {\n+            if (LOGGER.isDebugEnabled()) {\n+                LOGGER.debug(\"close \" + sessionName());","path":"agent/src/com/thoughtworks/go/agent/service/AgentWebsocketService.java","position":137,"original_position":137,"commit_id":"14f150c6768a52be73f720d4eb5c97a42bcf9e07","original_commit_id":"14f150c6768a52be73f720d4eb5c97a42bcf9e07","user":{"login":"ketan","id":10598,"avatar_url":"https://avatars.githubusercontent.com/u/10598?v=3","gravatar_id":"","url":"https://api.github.com/users/ketan","html_url":"https://github.com/ketan","followers_url":"https://api.github.com/users/ketan/followers","following_url":"https://api.github.com/users/ketan/following{/other_user}","gists_url":"https://api.github.com/users/ketan/gists{/gist_id}","starred_url":"https://api.github.com/users/ketan/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ketan/subscriptions","organizations_url":"https://api.github.com/users/ketan/orgs","repos_url":"https://api.github.com/users/ketan/repos","events_url":"https://api.github.com/users/ketan/events{/privacy}","received_events_url":"https://api.github.com/users/ketan/received_events","type":"User","site_admin":false},"body":"With SLF4j — this can be rewritten as `LOGGER.debug(\"close {}\", sessionName())`. The actual string interpolation only happens if the message needs to be logged. You may drop the `isDebugEnabled` as well.","created_at":"2016-01-28T07:32:21Z","updated_at":"2016-01-28T07:32:21Z","html_url":"https://github.com/gocd/gocd/pull/1793#discussion_r51088543","pull_request_url":"https://api.github.com/repos/gocd/gocd/pulls/1793","_links":{"self":{"href":"https://api.github.com/repos/gocd/gocd/pulls/comments/51088543"},"html":{"href":"https://github.com/gocd/gocd/pull/1793#discussion_r51088543"},"pull_request":{"href":"https://api.github.com/repos/gocd/gocd/pulls/1793"}}},"pull_request":{"url":"https://api.github.com/repos/gocd/gocd/pulls/1793","id":55793538,"html_url":"https://github.com/gocd/gocd/pull/1793","diff_url":"https://github.com/gocd/gocd/pull/1793.diff","patch_url":"https://github.com/gocd/gocd/pull/1793.patch","issue_url":"https://api.github.com/repos/gocd/gocd/issues/1793","number":1793,"state":"open","locked":false,"title":"Websocket communication (#1098)","user":{"login":"xli","id":17474,"avatar_url":"https://avatars.githubusercontent.com/u/17474?v=3","gravatar_id":"","url":"https://api.github.com/users/xli","html_url":"https://github.com/xli","followers_url":"https://api.github.com/users/xli/followers","following_url":"https://api.github.com/users/xli/following{/other_user}","gists_url":"https://api.github.com/users/xli/gists{/gist_id}","starred_url":"https://api.github.com/users/xli/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/xli/subscriptions","organizations_url":"https://api.github.com/users/xli/orgs","repos_url":"https://api.github.com/users/xli/repos","events_url":"https://api.github.com/users/xli/events{/privacy}","received_events_url":"https://api.github.com/users/xli/received_events","type":"User","site_admin":false},"body":"Implements websocket communication between agent and server (#1098 and #1125).\r\n\r\n1. Use Jetty websocket in both client and server side\r\n2. Server side push:\r\n    1. new job\r\n    2. set cookie\r\n    3. cancel job\r\n3. Agent will still ping server with agent runtime info periodically for:\r\n    1. keep connection alive.\r\n    2. robustness\r\n4. A toggle (system environment property) in Agent side for switching to use websocket communication.\r\nThis toggle is designed for testing and continuous improvement before we can get ride of current spring RPC implementation.\r\n5. More details and some design decisions see the following discussions\r\n6. Message format is using Java serialization due to the complexity of BuildWork object.\r\n\r\n---\r\n\r\n**Edit:** (copying the design decisions here in case @xli ever removes his fork in the future) \r\n\r\n---\r\n\r\n### Overview\r\n\r\nWhen I started working on this PR, I did some analysis about how existing PRC agent works and how I'd like websocket version works, and captured here: https://github.com/xli/asset/blob/master/go-cd-agent-design-sequence-diagrams.JPG\r\n\r\nIt should give you an overview of how the system works.\r\nComparing with my final implementation, it only has a little bit different in details (several events order, and missing how reschedule job works).\r\n\r\n### How server push job to agent?\r\n\r\n* BuildAssignmentServer#onTimer triggers job plans refresh, then start to find matching job for idling agents that is registered by websocket. \r\n* Assign work to agent message will be sent asynchronously, thus it won't block #onTimer method.\r\n* Server won't push blank work (NoWork) to agent (PRC behavior).\r\n\r\n### How unregistered and disabled agents are handled on server?\r\n\r\n1. Server always accepts agent websocket session no matter what agent status is when agent pings server. Hence an unregistered agent will be accepted. This is same logic with RPC version that will accept \"getWork\" call.\r\n2. Server will send reregister message to agent if it found agent was not registered during the process of matching agents with jobs (similar with current RPC behavior). We don't handle it when websocket session is opened, because agent may become unregistered status if user deleted agent from agent list. So handle it in one place is robust enough and simpler.\r\n3. However, we won't do anything to disabled agents. (agent will get NoWork in current RPC version)\r\n4. Do we really need the complicated SSL certificate management we do today?\r\n  * I try to avoid this discussion by implementing almost exactly same behavior, because of my lag knowledge of these code\r\n\r\n### Why Server sends message to Agent in asynchronous mode? And how we'll handle asynchronous sending error?\r\n\r\nThis is designed for speed up all of processing with agent, especially we don't want to block and fail the process of matching agents with jobs. \r\n\r\nWe rely on the way we handle websocket connection and rescheduling hanging jobs to handle sending message IO failures, which may need more test to find out how likely it will happen. \r\n\r\nThe sending message failure will be logged by WebSocket onError method.\r\n\r\nSee question of [\"What we do for handling websocket error?\"](#what-we-do-for-handling-websocket-error-on-server) for more details of why we can ignore asynch sending message failure.\r\n\r\n### When server found an agent lost connection\r\n\r\nServer will mark this agent as lost contact and remove it from the agent list for matching agents and jobs.\r\nThis may cause server shows agent status \"lost contact\" temporary if the agent reconnects to server immediately.\r\n\r\n### What happened when agent lost websocket connection and is building something?\r\n\r\n* Server(AgentRemoteSocket) will receive connection close event and remove websocket session, then mark agent lost contact\r\n* ScheduleService#rescheduleHungJobs (about 5 mins) will reschedule job if agent didn't reconnect.\r\n* Agent reconnect to server building status, and it is still building the original job, if agent didn't restart. The running job should be rescheduled by ScheduleService#rescheduleHungJobs\r\n* If Agent restarted, agent will connect to server with idle status, which will update server side agent status. The running job should be rescheduled when reconnected agent is assigned with a new job.\r\n\r\n### What we do for handling websocket error on Server?\r\n\r\nWe log error and do nothing else. Our other parts of system will handle cases caused by these errors.\r\nWe'll go through all possible errors and verify it.\r\n\r\n* There are 3 cases for sending messages:\r\n   1. send \"cancel\", \"setCookie\": these 2 actions are triggered when agent pings server. Agent pings server periodically, hence these action will be triggered again and safe to ignore error.\r\n   2. send \"reregister\": server does match agent with jobs periodically, hence this action will be triggered again.\r\n   3. send \"assignWork\": when this message failed, agent status on server is changed to building and agent status on client is still idle. Hence when agent pings server, agent will be updated to idle status, which is similar case with \"when agent is restarted (reconnected to server) just after assigned a new work\" (see question [What happened when agent lost websocket connection and is building something?](#what-happened-when-agent-lost-websocket-connection-and-is-building-something) for details).\r\n* Connection idle timeout:\r\n   1. This error will cause connection closed, and mark agent lost contact. And it is same case with Agent stopped if this agent never connected again, or restarted if this agent reconnected later.\r\n   2. Default connection timeout is 1 minute, and it can be changed by system property \"go.websocket.max.idle.time\".\r\n   3. Agent should ping server within 1 minute. It is 10 seconds right now due to RPC getWork periodical interval setting, we can discuss it later to find a better interval.\r\n* Receive message error: this is agent/client problem, it should not cause any problem as ping failed. And ping is a periodical task, hence it will be recovered if it was temp problem, and it's fine to ignore this error.\r\n* Decode message error: this is server side bug, or mismatch encoding problem between agent and server. It causes agent is never added into waiting new job list (because ping failed), hence nothing we can do to recover, log error.\r\n* Process message error: server runtime error, nothing we can do to recover, log error.\r\n\r\n### What we do for handling websocket error on Client (Agent)?\r\n\r\n* We do synch sending message, hence any error when sending message will cause error and stop current thread.\r\n* It basically should be similar behavior with RPC version\r\n* log error\r\n* when ping failed, it's fine to ignore it, because ping itself is a periodical task\r\n* when reportCurrentStatus, reportCompleted, or reportCompleting failed during running job, it will fail the job running thread and agent will be set to idle status\r\n\r\n### How we convert BuildRepositoryRemote#isIgnored(JobIdentifier jobIdentifier)?\r\n\r\n* Is ignored will check given job status, if it is canceled or rescheduled then return true.\r\n* So, new implementation will change to monitor JobRunner isCancelled status.\r\n* Then server will push Cancel message to agent if job is cancelled or rescheduled.\r\n* The worst case is agent run job finished because cancel failed or before agent received cancel message, which in theory also happens in RPC mode.\r\n\r\n### Multiple agents at same machine\r\n\r\n* No extra configuration needed, tested\r\n\r\n### Toggle for turn on agent websocket communication\r\n\r\nThere is no toggle on Go server, always available.\r\nGo Agent: add GO_AGENT_SYSTEM_PROPERTIES: -Dgo.agent.websocket.enabled=Y\r\n\r\n### Other system environment variables:\r\n\r\nServer and Agent should use same configuration:\r\n* go.websocket.max.idle.time, default 60 seconds\r\n* go.websocket.max.text.message.size, default 128K\r\n\r\n\r\n# Test\r\n\r\n* ✓ multiple agents on same machine\r\n* ✓ upgrade from RPC agent to websocket agent (enabled by default)\r\n* ✓ Server behind a http proxy: agent will fetch ssl port from server, which means, there is no way to configure agent connect to proxy server port, so if there is proxy server between agent and server, it need act exactly same with server including open same ssl port. Then we need export Go server's ssl certificates, I was blocked here because I could not figure out what's keystore's password :(. Anyway, it seems we don't expect anyone has proxy between server and agent, so I'll stop here\r\n* ✓ No test failure if we turned on go.agent.websocket.enabled by default\r\n* AC/smoke test suite\r\n\r\n","created_at":"2016-01-12T17:56:20Z","updated_at":"2016-01-28T07:32:21Z","closed_at":null,"merged_at":null,"merge_commit_sha":"8c2f078556c0ed6a31c6c9cf4a43759af2984ec3","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/gocd/gocd/pulls/1793/commits","review_comments_url":"https://api.github.com/repos/gocd/gocd/pulls/1793/comments","review_comment_url":"https://api.github.com/repos/gocd/gocd/pulls/comments{/number}","comments_url":"https://api.github.com/repos/gocd/gocd/issues/1793/comments","statuses_url":"https://api.github.com/repos/gocd/gocd/statuses/14f150c6768a52be73f720d4eb5c97a42bcf9e07","head":{"label":"xli:jetty.websocket","ref":"jetty.websocket","sha":"14f150c6768a52be73f720d4eb5c97a42bcf9e07","user":{"login":"xli","id":17474,"avatar_url":"https://avatars.githubusercontent.com/u/17474?v=3","gravatar_id":"","url":"https://api.github.com/users/xli","html_url":"https://github.com/xli","followers_url":"https://api.github.com/users/xli/followers","following_url":"https://api.github.com/users/xli/following{/other_user}","gists_url":"https://api.github.com/users/xli/gists{/gist_id}","starred_url":"https://api.github.com/users/xli/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/xli/subscriptions","organizations_url":"https://api.github.com/users/xli/orgs","repos_url":"https://api.github.com/users/xli/repos","events_url":"https://api.github.com/users/xli/events{/privacy}","received_events_url":"https://api.github.com/users/xli/received_events","type":"User","site_admin":false},"repo":{"id":47584139,"name":"gocd","full_name":"xli/gocd","owner":{"login":"xli","id":17474,"avatar_url":"https://avatars.githubusercontent.com/u/17474?v=3","gravatar_id":"","url":"https://api.github.com/users/xli","html_url":"https://github.com/xli","followers_url":"https://api.github.com/users/xli/followers","following_url":"https://api.github.com/users/xli/following{/other_user}","gists_url":"https://api.github.com/users/xli/gists{/gist_id}","starred_url":"https://api.github.com/users/xli/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/xli/subscriptions","organizations_url":"https://api.github.com/users/xli/orgs","repos_url":"https://api.github.com/users/xli/repos","events_url":"https://api.github.com/users/xli/events{/privacy}","received_events_url":"https://api.github.com/users/xli/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/xli/gocd","description":"Main repository for Go Continuous Delivery","fork":true,"url":"https://api.github.com/repos/xli/gocd","forks_url":"https://api.github.com/repos/xli/gocd/forks","keys_url":"https://api.github.com/repos/xli/gocd/keys{/key_id}","collaborators_url":"https://api.github.com/repos/xli/gocd/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/xli/gocd/teams","hooks_url":"https://api.github.com/repos/xli/gocd/hooks","issue_events_url":"https://api.github.com/repos/xli/gocd/issues/events{/number}","events_url":"https://api.github.com/repos/xli/gocd/events","assignees_url":"https://api.github.com/repos/xli/gocd/assignees{/user}","branches_url":"https://api.github.com/repos/xli/gocd/branches{/branch}","tags_url":"https://api.github.com/repos/xli/gocd/tags","blobs_url":"https://api.github.com/repos/xli/gocd/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/xli/gocd/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/xli/gocd/git/refs{/sha}","trees_url":"https://api.github.com/repos/xli/gocd/git/trees{/sha}","statuses_url":"https://api.github.com/repos/xli/gocd/statuses/{sha}","languages_url":"https://api.github.com/repos/xli/gocd/languages","stargazers_url":"https://api.github.com/repos/xli/gocd/stargazers","contributors_url":"https://api.github.com/repos/xli/gocd/contributors","subscribers_url":"https://api.github.com/repos/xli/gocd/subscribers","subscription_url":"https://api.github.com/repos/xli/gocd/subscription","commits_url":"https://api.github.com/repos/xli/gocd/commits{/sha}","git_commits_url":"https://api.github.com/repos/xli/gocd/git/commits{/sha}","comments_url":"https://api.github.com/repos/xli/gocd/comments{/number}","issue_comment_url":"https://api.github.com/repos/xli/gocd/issues/comments{/number}","contents_url":"https://api.github.com/repos/xli/gocd/contents/{+path}","compare_url":"https://api.github.com/repos/xli/gocd/compare/{base}...{head}","merges_url":"https://api.github.com/repos/xli/gocd/merges","archive_url":"https://api.github.com/repos/xli/gocd/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/xli/gocd/downloads","issues_url":"https://api.github.com/repos/xli/gocd/issues{/number}","pulls_url":"https://api.github.com/repos/xli/gocd/pulls{/number}","milestones_url":"https://api.github.com/repos/xli/gocd/milestones{/number}","notifications_url":"https://api.github.com/repos/xli/gocd/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/xli/gocd/labels{/name}","releases_url":"https://api.github.com/repos/xli/gocd/releases{/id}","deployments_url":"https://api.github.com/repos/xli/gocd/deployments","created_at":"2015-12-07T22:48:12Z","updated_at":"2016-01-13T20:34:09Z","pushed_at":"2016-01-26T19:35:44Z","git_url":"git://github.com/xli/gocd.git","ssh_url":"git@github.com:xli/gocd.git","clone_url":"https://github.com/xli/gocd.git","svn_url":"https://github.com/xli/gocd","homepage":"http://go.cd","size":225570,"stargazers_count":0,"watchers_count":0,"language":"Java","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"gocd:master","ref":"master","sha":"75f21bf7bebddc15fadf65651b709dbb4961ddbe","user":{"login":"gocd","id":6099846,"avatar_url":"https://avatars.githubusercontent.com/u/6099846?v=3","gravatar_id":"","url":"https://api.github.com/users/gocd","html_url":"https://github.com/gocd","followers_url":"https://api.github.com/users/gocd/followers","following_url":"https://api.github.com/users/gocd/following{/other_user}","gists_url":"https://api.github.com/users/gocd/gists{/gist_id}","starred_url":"https://api.github.com/users/gocd/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/gocd/subscriptions","organizations_url":"https://api.github.com/users/gocd/orgs","repos_url":"https://api.github.com/users/gocd/repos","events_url":"https://api.github.com/users/gocd/events{/privacy}","received_events_url":"https://api.github.com/users/gocd/received_events","type":"Organization","site_admin":false},"repo":{"id":15155534,"name":"gocd","full_name":"gocd/gocd","owner":{"login":"gocd","id":6099846,"avatar_url":"https://avatars.githubusercontent.com/u/6099846?v=3","gravatar_id":"","url":"https://api.github.com/users/gocd","html_url":"https://github.com/gocd","followers_url":"https://api.github.com/users/gocd/followers","following_url":"https://api.github.com/users/gocd/following{/other_user}","gists_url":"https://api.github.com/users/gocd/gists{/gist_id}","starred_url":"https://api.github.com/users/gocd/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/gocd/subscriptions","organizations_url":"https://api.github.com/users/gocd/orgs","repos_url":"https://api.github.com/users/gocd/repos","events_url":"https://api.github.com/users/gocd/events{/privacy}","received_events_url":"https://api.github.com/users/gocd/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/gocd/gocd","description":"Main repository for Go Continuous Delivery","fork":false,"url":"https://api.github.com/repos/gocd/gocd","forks_url":"https://api.github.com/repos/gocd/gocd/forks","keys_url":"https://api.github.com/repos/gocd/gocd/keys{/key_id}","collaborators_url":"https://api.github.com/repos/gocd/gocd/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/gocd/gocd/teams","hooks_url":"https://api.github.com/repos/gocd/gocd/hooks","issue_events_url":"https://api.github.com/repos/gocd/gocd/issues/events{/number}","events_url":"https://api.github.com/repos/gocd/gocd/events","assignees_url":"https://api.github.com/repos/gocd/gocd/assignees{/user}","branches_url":"https://api.github.com/repos/gocd/gocd/branches{/branch}","tags_url":"https://api.github.com/repos/gocd/gocd/tags","blobs_url":"https://api.github.com/repos/gocd/gocd/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/gocd/gocd/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/gocd/gocd/git/refs{/sha}","trees_url":"https://api.github.com/repos/gocd/gocd/git/trees{/sha}","statuses_url":"https://api.github.com/repos/gocd/gocd/statuses/{sha}","languages_url":"https://api.github.com/repos/gocd/gocd/languages","stargazers_url":"https://api.github.com/repos/gocd/gocd/stargazers","contributors_url":"https://api.github.com/repos/gocd/gocd/contributors","subscribers_url":"https://api.github.com/repos/gocd/gocd/subscribers","subscription_url":"https://api.github.com/repos/gocd/gocd/subscription","commits_url":"https://api.github.com/repos/gocd/gocd/commits{/sha}","git_commits_url":"https://api.github.com/repos/gocd/gocd/git/commits{/sha}","comments_url":"https://api.github.com/repos/gocd/gocd/comments{/number}","issue_comment_url":"https://api.github.com/repos/gocd/gocd/issues/comments{/number}","contents_url":"https://api.github.com/repos/gocd/gocd/contents/{+path}","compare_url":"https://api.github.com/repos/gocd/gocd/compare/{base}...{head}","merges_url":"https://api.github.com/repos/gocd/gocd/merges","archive_url":"https://api.github.com/repos/gocd/gocd/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/gocd/gocd/downloads","issues_url":"https://api.github.com/repos/gocd/gocd/issues{/number}","pulls_url":"https://api.github.com/repos/gocd/gocd/pulls{/number}","milestones_url":"https://api.github.com/repos/gocd/gocd/milestones{/number}","notifications_url":"https://api.github.com/repos/gocd/gocd/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/gocd/gocd/labels{/name}","releases_url":"https://api.github.com/repos/gocd/gocd/releases{/id}","deployments_url":"https://api.github.com/repos/gocd/gocd/deployments","created_at":"2013-12-13T05:26:16Z","updated_at":"2016-01-28T07:25:57Z","pushed_at":"2016-01-28T07:19:58Z","git_url":"git://github.com/gocd/gocd.git","ssh_url":"git@github.com:gocd/gocd.git","clone_url":"https://github.com/gocd/gocd.git","svn_url":"https://github.com/gocd/gocd","homepage":"https://go.cd","size":294478,"stargazers_count":2074,"watchers_count":2074,"language":"Java","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":362,"mirror_url":null,"open_issues_count":603,"forks":362,"open_issues":603,"watchers":2074,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/gocd/gocd/pulls/1793"},"html":{"href":"https://github.com/gocd/gocd/pull/1793"},"issue":{"href":"https://api.github.com/repos/gocd/gocd/issues/1793"},"comments":{"href":"https://api.github.com/repos/gocd/gocd/issues/1793/comments"},"review_comments":{"href":"https://api.github.com/repos/gocd/gocd/pulls/1793/comments"},"review_comment":{"href":"https://api.github.com/repos/gocd/gocd/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/gocd/gocd/pulls/1793/commits"},"statuses":{"href":"https://api.github.com/repos/gocd/gocd/statuses/14f150c6768a52be73f720d4eb5c97a42bcf9e07"}}}}