Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building GitHub Integrations with Webhooks and REST

Building GitHub Integrations with Webhooks and REST

Webhooks are valuable tools for powering real-time integrations and workflows in your project’s existing tools. This session will walk through an approach of building a webhook-powered application that acts on events as they take place on GitHub. You will also learn how to use the GitHub REST API through the Octokit SDK to call for additional resources after receiving an event.

359024e7132672aaeef4a3e792be4ae5?s=128

Brooks Swinnerton

May 07, 2020
Tweet

Transcript

  1. > sessions/workshops/ Building GitHub integrations with webhooks and REST Brooks

    Swinnerton Jane Sternbach John Tzikas
  2. Brooks Swinnerton Engineering Manager, Ecosystem Events Jane Sternbach Senior Software

    Engineer, Ecosystem Events John Tzikas Senior Software Engineer, Ecosystem Events
  3. Today’s agenda 1. Introduction to REST APIs
 2. Introduction to

    Webhooks
 3. Webhooks best practices
 4. Workshop: Building an integration
  4. Today’s agenda 1. Introduction to REST APIs
 2. Introduction to

    Webhooks
 3. Webhooks best practices
 4. Workshop: Building an integration
  5. Today’s agenda 1. Introduction to REST APIs
 2. Introduction to

    Webhooks
 3. Webhooks best practices
 4. Workshop: Building an integration
  6. Today’s agenda 1. Introduction to REST APIs
 2. Introduction to

    Webhooks
 3. Webhooks best practices
 4. Workshop: Building an integration
  7. Today’s agenda 1. Introduction to REST APIs
 2. Introduction to

    Webhooks
 3. Webhooks best practices
 4. Workshop: Building an integration
  8. None
  9. REST APIs

  10. a way of programmatically getting data

  11. HTTP Request / Response Lifecycle

  12. GET https://api.github.com/user

  13. GET https://api.github.com/user └─┘ verb

  14. GET https://api.github.com/user └─┘ verb └───────────────┘ endpoint

  15. ┌─────┐ ┌────────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ GET /user │ │ │e │ │───────────────────────────────────▶│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ You │ │ GitHub │ │ │ │◀───────────────────────────────────│ │ │ │ │ │ │ │ │ │ { │ │ │ │ │ "login": "bswinnerton", │ │ │ │ │ ... │ │ │ │ │ } │ │ │ │ │ │ │ │ │ │ │ │ │ └─────┘ └────────┘ ▼ > REST API
  16. ┌─────┐ ┌────────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ GET /user │ │ │e │ │───────────────────────────────────▶│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ You │ │ GitHub │ │ │ │◀───────────────────────────────────│ │ │ │ │ │ │ │ │ │ { │ │ │ │ │ "login": "bswinnerton", │ │ │ │ │ ... │ │ │ │ │ } │ │ │ │ │ │ │ │ │ │ │ │ │ └─────┘ └────────┘ ▼ > REST API
  17. ┌─────┐ ┌────────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ GET /user │ │ │e │ │───────────────────────────────────▶│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ You │ │ GitHub │ │ │ │◀───────────────────────────────────│ │ │ │ │ │ │ │ │ │ { │ │ │ │ │ "login": "bswinnerton", │ │ │ │ │ ... │ │ │ │ │ } │ │ │ │ │ │ │ │ │ │ │ │ │ └─────┘ └────────┘ ▼ > REST API
  18. { "login": "bswinnerton", "id": 934497, "name": "Brooks Swinnerton", "company": "GitHub",

    "blog": "https://brooks.sh", "location": "Brooklyn, NY", "bio": "Ecosystem Engineering Manager", "public_repos": 41, "public_gists": 56, "followers": 620, "following": 68, "created_at": "2011-07-23T17:44:47Z", "updated_at": "2020-04-25T00:15:50Z" } > JSON
  19. how does this work in practice?

  20. $ curl -X GET https://api.github.com/user

  21. client libraries

  22. client = Octokit::Client.new( login: 'monalisa', password: 'correcthorsebatterystaple' ) client.user

  23. const { Octokit } = require("@octokit/rest"); const octokit = new

    Octokit(); octokit.users.getAuthenticated();
  24. REST APIs are based on a “pull” model

  25. it’s hard to achieve real time in this model

  26. ┌─────┐ ┌────────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ GET /pull_requests/:id │ │ │e │ │───────────────────────────────────▶│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ You │ │ GitHub │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─────┘ └────────┘ ▼ > REST API
  27. ┌─────┐ ┌────────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ GET /pull_requests/:id │ │ │e │ │───────────────────────────────────▶│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ You │ │ GitHub │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─────┘ └────────┘ ▼ > REST API
  28. ┌─────┐ ┌────────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ GET /pull_requests/:id │ │ │e │ │───────────────────────────────────▶│ │ │ │ │◀───────────────────────────────────│ │ │ │ │ HTTP 304 Not Modified │ │ │ │ │ │ │ │ │ You │ │ GitHub │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─────┘ └────────┘ ▼ > REST API
  29. ┌─────┐ ┌────────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ GET /pull_requests/:id │ │ │e │ │───────────────────────────────────▶│ │ │ │ │◀───────────────────────────────────│ │ │ │ │ HTTP 304 Not Modified │ │ │ │ │ │ │ │ │ You │ GET /pull_requests/:id │ GitHub │ │ │ │───────────────────────────────────▶│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─────┘ └────────┘ ▼ > REST API
  30. ┌─────┐ ┌────────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ GET /pull_requests/:id │ │ │e │ │───────────────────────────────────▶│ │ │ │ │◀───────────────────────────────────│ │ │ │ │ HTTP 304 Not Modified │ │ │ │ │ │ │ │ │ You │ GET /pull_requests/:id │ GitHub │ │ │ │───────────────────────────────────▶│ │ │ │ │◀───────────────────────────────────│ │ │ │ │ HTTP 304 Not Modified │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─────┘ └────────┘ ▼ > REST API
  31. ┌─────┐ ┌────────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ GET /pull_requests/:id │ │ │e │ │───────────────────────────────────▶│ │ │ │ │◀───────────────────────────────────│ │ │ │ │ HTTP 304 Not Modified │ │ │ │ │ │ │ │ │ You │ GET /pull_requests/:id │ GitHub │ │ │ │───────────────────────────────────▶│ │ │ │ │◀───────────────────────────────────│ │ │ │ │ HTTP 304 Not Modified │ │ │ │ │ │ │ │ │ │ GET /pull_requests/:id │ │ │ │ │───────────────────────────────────▶│ │ │ │ │ │ │ │ │ │ │ │ │ └─────┘ └────────┘ ▼ > REST API
  32. ┌─────┐ ┌────────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ GET /pull_requests/:id │ │ │e │ │───────────────────────────────────▶│ │ │ │ │◀───────────────────────────────────│ │ │ │ │ HTTP 304 Not Modified │ │ │ │ │ │ │ │ │ You │ GET /pull_requests/:id │ GitHub │ │ │ │───────────────────────────────────▶│ │ │ │ │◀───────────────────────────────────│ │ │ │ │ HTTP 304 Not Modified │ │ │ │ │ │ │ │ │ │ GET /pull_requests/:id │ │ │ │ │───────────────────────────────────▶│ │ │ │ │◀───────────────────────────────────│ │ │ │ │ HTTP 200 OK {...} │ │ │ └─────┘ └────────┘ ▼ > REST API
  33. rate limits

  34. what would a “push” model look like?

  35. Webhooks

  36. webhooks change the flow of data

  37. ┌─────┐ ┌────────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ GET /user │ │ │e │ │───────────────────────────────────▶│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ You │ │ GitHub │ │ │ │◀───────────────────────────────────│ │ │ │ │ │ │ │ │ │ { │ │ │ │ │ "login": "bswinnerton", │ │ │ │ │ ... │ │ │ │ │ } │ │ │ │ │ │ │ │ │ │ │ │ │ └─────┘ └────────┘ ▼ > REST API
  38. ┌────────┐ ┌─────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ POST / │ │ │e │ │───────────────────────────────▶│ │ │ │ │ │ │ │ │ │ { │ │ │ │ │ "action": "edited", │ │ │ │ GitHub │ ... │ You │ │ │ │ } │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │◀───────────────────────────────│ │ │ │ │ HTTP 200 OK │ │ │ │ │ │ │ │ └────────┘ └─────┘ ▼ > webhooks
  39. ┌────────┐ ┌─────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ POST / │ │ │e │ │───────────────────────────────▶│ │ │ │ │ │ │ │ │ │ { │ │ │ │ │ "action": "edited", │ │ │ │ GitHub │ ... │ You │ │ │ │ } │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │◀───────────────────────────────│ │ │ │ │ HTTP 200 OK │ │ │ │ │ │ │ │ └────────┘ └─────┘ ▼ > webhooks
  40. ┌────────┐ ┌─────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ POST / │ │ │e │ │───────────────────────────────▶│ │ │ │ │ │ │ │ │ │ { │ │ │ │ │ "action": "edited", │ │ │ │ GitHub │ ... │ You │ │ │ │ } │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │◀───────────────────────────────│ │ │ │ │ HTTP 200 OK │ │ │ │ │ │ │ │ └────────┘ └─────┘ ▼ > webhooks
  41. anatomy of a webhook

  42. HTTP/1.1 User-Agent: GitHub-Hookshot/ef5a70a X-Github-Delivery: d6552d80-8a73-11ea-83d4-d009fa985176 X-Github-Event: issue_comment { "action": "created",

    "comment": { "id": 621527989, "body": "Hi Mom” ... }, "sender": { "login": “bswinnerton", ... } ... }
  43. HTTP/1.1 User-Agent: GitHub-Hookshot/ef5a70a X-Github-Delivery: d6552d80-8a73-11ea-83d4-d009fa985176 X-Github-Event: issue_comment { "action": "created",

    "comment": { "id": 621527989, "body": "Hi Mom” ... }, "sender": { "login": “bswinnerton", ... } ... }
  44. HTTP/1.1 User-Agent: GitHub-Hookshot/ef5a70a X-Github-Delivery: d6552d80-8a73-11ea-83d4-d009fa985176 X-Github-Event: issue_comment { "action": "created",

    "comment": { "id": 621527989, "body": "Hi Mom” ... }, "sender": { "login": “bswinnerton", ... } ... }
  45. HTTP/1.1 User-Agent: GitHub-Hookshot/ef5a70a X-Github-Delivery: d6552d80-8a73-11ea-83d4-d009fa985176 X-Github-Event: issue_comment { "action": "created",

    "comment": { "id": 621527989, "body": "Hi Mom” ... }, "sender": { "login": “bswinnerton", ... } ... }
  46. HTTP/1.1 User-Agent: GitHub-Hookshot/ef5a70a X-Github-Delivery: d6552d80-8a73-11ea-83d4-d009fa985176 X-Github-Event: issue_comment { "action": "created",

    "comment": { "id": 621527989, "body": "Hi Mom" ... }, "sender": { "login": "bswinnerton", ... } ... }
  47. events & actions

  48. check_run check_suite commit_comment content_reference create delete deploy_key deployment deployment_status fork

    github_app_authorization gollum installation installation_repositories issue_comment issues label marketplace_purchase member membership meta milestone organization org_block page_build project_card project_column project public pull_request pull_request_review pull_request_review_comment push package release repository repository_import repository_vulnerability_alert security_advisory sponsorship star status team team_add watch * > developer.github.com/webhooks/
  49. check_run check_suite commit_comment content_reference create delete deploy_key deployment deployment_status fork

    github_app_authorization gollum installation installation_repositories issue_comment issues label marketplace_purchase member membership meta milestone organization org_block page_build project_card project_column project public pull_request pull_request_review pull_request_review_comment push package release repository repository_import repository_vulnerability_alert security_advisory sponsorship star status team team_add watch * > developer.github.com/webhooks/
  50. check_run check_suite commit_comment content_reference create delete deploy_key deployment deployment_status fork

    github_app_authorization gollum installation installation_repositories issue_comment issues label marketplace_purchase member membership meta milestone organization org_block page_build project_card project_column project public pull_request pull_request_review pull_request_review_comment push package release repository repository_import repository_vulnerability_alert security_advisory sponsorship star status team team_add watch * > developer.github.com/webhooks/
  51. check_run check_suite commit_comment content_reference create delete deploy_key deployment deployment_status fork

    github_app_authorization gollum installation installation_repositories issue_comment issues label marketplace_purchase member membership meta milestone organization org_block page_build project_card project_column project public pull_request pull_request_review pull_request_review_comment push package release repository repository_import repository_vulnerability_alert security_advisory sponsorship star status team team_add watch * > developer.github.com/webhooks/
  52. installation targets

  53. configuring a webhook

  54. None
  55. None
  56. requirements

  57. 1. A web server that’s listening for requests

  58. 2. An address on the internet that GitHub can reach

  59. ngrok

  60. ┌────────┐ ┌─────┐ ┌─────┐ │T │ │ │ │ │ │

    │i │ │ │ │ │ │ │m │ │ POST / │ │ POST / │ │ │e │ │────────────▶│ │────────────▶│ │ │ │ │ { ... } │ │ { ... } │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ GitHub │ │ngrok│ │ You │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │◀────────────│ │◀────────────│ │ │ │ │ HTTP 200 OK │ │ HTTP 200 OK │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └────────┘ └─────┘ └─────┘ ▼ > ngrok
  61. > ngrok ┌────────┐ ┌─────┐ ┌─────┐ │T │ │ │ │

    │ │ │i │ │ │ │ │ │ │m │ │ POST / │ │ POST / │ │ │e │ │────────────▶│ │────────────▶│ │ │ │ │ { ... } │ │ { ... } │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ GitHub │ │ngrok│ │ You │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │◀────────────│ │◀────────────│ │ │ │ │ HTTP 200 OK │ │ HTTP 200 OK │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └────────┘ └─────┘ └─────┘ ▼
  62. > ngrok ┌────────┐ ┌─────┐ ┌─────┐ │T │ │ │ │

    │ │ │i │ │ │ │ │ │ │m │ │ POST / │ │ POST / │ │ │e │ │────────────▶│ │────────────▶│ │ │ │ │ { ... } │ │ { ... } │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ GitHub │ │ngrok│ │ You │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │◀────────────│ │◀────────────│ │ │ │ │ HTTP 200 OK │ │ HTTP 200 OK │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └────────┘ └─────┘ └─────┘ ▼
  63. Best Practices for Receiving Webhooks

  64. Resiliency Security HTTP

  65. Resiliency Security HTTP

  66. respond to webhooks as quick as you can

  67. 10 second timeout

  68. ┌────────┐ ┌─────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ POST / │ │ │e │ │───────────────────────────────▶│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ GitHub │ │ You │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └────────┘ └─────┘ ▼ > webhooks
  69. ┌────────┐ ┌─────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ POST / │ │ │e │ │───────────────────────────────▶│ │ │ │ │────────────────────────────▶ │ │ │ │ │ │ │ │ │ │ │ │ │ │ GitHub │ │ You │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └────────┘ └─────┘ ▼ > webhooks
  70. ┌────────┐ ┌─────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ POST / │ │ │e │ │───────────────────────────────▶│ │ │ │ │────────────────────────────▶ │ │ │ │ │─────────────────────────▶ │ │ │ │ │ │ │ │ │ GitHub │ │ You │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └────────┘ └─────┘ ▼ > webhooks
  71. order of events is best effort

  72. Resiliency Security HTTP

  73. 
 verify authenticity with HMAC signatures

  74. ┌────────┐ ┌────────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ POST / │ │ │e │ │───────────────────────────────▶│ │ │ │ │ │ │ │ │ │ { │ │ │ │ │ "action": "edited", │ │ │ │ GitHub │ ... │ You │ │ │ │ } │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │◀───────────────────────────────│ │ │ │ │ HTTP 200 OK │ │ │ │ │ │ │ │ └────────┘ └────────┘ ▼ > webhooks
  75. ┌────────┐ ┌────────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ POST / │ │ │e │ │───────────────────────────────▶│ │ │ │ │ │ │ │ │ │ { │ │ │ │ │ "action": "edited", │ │ │ │ Evil │ ... │ You │ │ │ Person │ } │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │◀───────────────────────────────│ │ │ │ │ HTTP 200 OK │ │ │ │ │ │ │ │ └────────┘ └────────┘ ▼ > malicious webhooks
  76. ┌────────┐ ┌────────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ POST / │ │ │e │ │───────────────────────────────▶│ │ │ │ │ │ │ │ │ │ { │ │ │ │ │ "action": "edited", │ │ │ │ Evil │ ... │ You │ │ │ Person │ } │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │◀───────────────────────────────│ │ │ │ │ HTTP 200 OK │ │ │ │ │ │ │ │ └────────┘ └────────┘ ▼ > malicious webhooks
  77. ┌────────┐ ┌────────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ POST / │ │ │e │ │───────────────────────────────▶│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ GitHub │ │ You │ │ │ │ │ │ │ │(secret)│ │(secret)│ │ │(abcdef)│ │(abcdef)│ │ │ │ │ │ │ │ │ │ │ │ │ │◀───────────────────────────────│ │ │ │ │ HTTP 200 OK │ │ │ │ │ │ │ │ └────────┘ └────────┘ ▼ > shared secret
  78. ┌────────┐ ┌────────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ POST / │ │ │e │ │───────────────────────────────▶│ │ │ │ │ x-hub-signature: f0ad5b │ │ │ │ │ │ │ │ │ │ { │ │ │ │ GitHub │ "action": "edited", │ You │ │ │ │ ... │ │ │ │(secret)│ } │(secret)│ │ │(abcdef)│ │(abcdef)│ │ │ │ │ │ │ │ │ │ │ │ │ │◀───────────────────────────────│ │ │ │ │ HTTP 200 OK │ │ │ │ │ │ │ │ └────────┘ └────────┘ ▼ > HMAC signatures
  79. ┌────────┐ ┌────────┐ │T │ │ │ │ │i │ │

    │ │ │m │ │ POST / │ │ │e │ │───────────────────────────────▶│ │ │ │ │ x-hub-signature: f0ad5b ✔ │ │ │ │ │ │ │ │ │ │ { │ │ │ │ GitHub │ "action": "edited", ✔ │ You │ │ │ │ ... │ │ │ │(secret)│ } │(secret)│ │ │(abcdef)│ │(abcdef)│ │ │ │ │ │ │ │ │ │ │ │ │ │◀───────────────────────────────│ │ │ │ │ HTTP 200 OK │ │ │ │ │ │ │ │ └────────┘ └────────┘ ▼ > HMAC signatures
  80. None
  81. IP whitelist

  82. $ curl https://api.github.com/meta

  83. Resiliency Security HTTP

  84. use sensible HTTP status codes

  85. 2xx: success 4xx: client error 5xx: server error

  86. None
  87. send GitHub helpful error messages for debugging

  88. None
  89. keep track of GUIDs

  90. HTTP/1.1 User-Agent: GitHub-Hookshot/ef5a70a X-Github-Delivery: d6552d80-8a73-11ea-83d4-d009fa985176 X-Github-Event: issue_comment { "action": "created",

    "comment": { "id": 621527989, "body": "Hi Mom” ... }, "sender": { "login": “bswinnerton", ... } ... }
  91. tools

  92. • As well as Insomnia & Paw postman

  93. curl + jq $ curl -s https://api.github.com/meta | jq .hooks[0]

    "192.30.252.0/22"
  94. • As well as requestbin and hookbin ngrok

  95. Workshop

  96. • Auto-updating changelog • Hosted on GitHub Pages Today’s goal

  97. • How to develop & test hooks • How to

    make REST API calls via Octokit • Best practices with webhooks What to expect
  98. • Terminal • Editor • Docker • Browser • Your

    repository • GitHub pages site • Webhook deliveries page Preparation
  99. Workshop