Tayori.GitHub (Tayori v0.1.0)

Copy Markdown View Source

Changes from GitHub: a webhook of the repository, at POST /hooks/github of the JSON API (Tayori.API). The Automation guide adds it in GitHub.

GitHub signs each delivery with the webhook's secret (x-hub-signature-256), and Tayori checks it against the body as it came. The admin makes the secret, new_secret/0, and pastes it into GitHub; Tayori keeps it encrypted, as the AI key (the :vault or the :secret_key_base of Tayori.Config).

Two events make changes:

  • A pull request merged into the default branch: one change, its :ref the repository and the number ("owner/repo#123"). Its title is the pull request's, and its body the section of the pull request's description headed ## Changelog or ## Release notes, when there is one: never the whole description, which is often for the team. A pull request closed without a merge is nothing.
  • A release published: each item of its notes, as Tayori.Sources.Changelog reads them. Items under ### Added have that kind; items with no section have none. Notes with no item are one change, titled with the release's name. A prerelease is nothing yet.

A pull request's labels say what the change is, with the prefix of the :github option of the :inbox config ("changelog:"):

  • changelog:skip - no change.
  • changelog:internal - for the team only; changelog:public - for everyone.
  • changelog:highlight - worth leading with.
  • changelog:category:<key> - its category.
  • changelog:segment:<segment> - the readers it is for (one label each).
  • changelog:<kind> - its kind (changelog:added, changelog:fixed), and so its category (the :kinds of the :inbox config).

With only_labeled: true, a pull request with none of these labels is nothing.

The raw body

The signature is of the body as GitHub sent it. An endpoint whose Plug.Parsers reads JSON (a Phoenix app's does) reads the body before the router; read_body/2 keeps a copy of it for the requests GitHub signs, and only for them:

plug Plug.Parsers,
  parsers: [:urlencoded, :multipart, :json],
  pass: ["*/*"],
  body_reader: {Tayori.GitHub, :read_body, []},
  json_decoder: Phoenix.json_library()

Without it, a delivery is answered with a 500, and the log says why.

Summary

Functions

The section of a pull request's description for its readers: the text under a heading Changelog, Change log or Release notes, to the next heading of its level, without HTML comments. Nil when there is no such section, or it is empty.

The changes of an event of GitHub, x-github-event and its payload: {:ok, changes} (none for a ping, or a pull request that is not news), or :ignore for an event Tayori does not read. The options are those of the :github option of the :inbox config, which they replace.

Makes the webhook's secret, in place of any other, and returns it, to paste into GitHub. Tayori keeps it encrypted, and never shows it again. {:error, :no_secrets} without a :vault or a :secret_key_base.

A body reader for Plug.Parsers (body_reader: {Tayori.GitHub, :read_body, []}): it reads the body as Plug.Conn.read_body/2 does, and keeps a copy of a body GitHub signed, for verify/2.

Removes the webhook's secret: GitHub's deliveries get a 404.

Whether there is a webhook secret.

Checks the signature of a body, the x-hub-signature-256 header (sha256=<hex>), in constant time. Returns :ok, {:error, :no_secret}, or {:error, :invalid}.

Functions

changelog_section(text)

@spec changelog_section(String.t() | nil) :: String.t() | nil

The section of a pull request's description for its readers: the text under a heading Changelog, Change log or Release notes, to the next heading of its level, without HTML comments. Nil when there is no such section, or it is empty.

iex> Tayori.GitHub.changelog_section("Fixes #12.\n\n## Changelog\n\nExports are faster.\n\n## Tests\n\nAdded.")
"Exports are faster."

handle(event, payload, opts \\ [])

@spec handle(String.t(), map(), keyword()) ::
  {:ok, [Tayori.Source.change()]} | :ignore

The changes of an event of GitHub, x-github-event and its payload: {:ok, changes} (none for a ping, or a pull request that is not news), or :ignore for an event Tayori does not read. The options are those of the :github option of the :inbox config, which they replace.

new_secret()

@spec new_secret() :: {:ok, String.t()} | {:error, :no_secrets}

Makes the webhook's secret, in place of any other, and returns it, to paste into GitHub. Tayori keeps it encrypted, and never shows it again. {:error, :no_secrets} without a :vault or a :secret_key_base.

read_body(conn, opts)

@spec read_body(Plug.Conn.t(), keyword()) ::
  {:ok, binary(), Plug.Conn.t()}
  | {:more, binary(), Plug.Conn.t()}
  | {:error, term()}

A body reader for Plug.Parsers (body_reader: {Tayori.GitHub, :read_body, []}): it reads the body as Plug.Conn.read_body/2 does, and keeps a copy of a body GitHub signed, for verify/2.

remove_secret()

@spec remove_secret() :: :ok

Removes the webhook's secret: GitHub's deliveries get a 404.

secret?()

@spec secret?() :: boolean()

Whether there is a webhook secret.

verify(body, signature)

@spec verify(binary(), String.t() | nil) :: :ok | {:error, :no_secret | :invalid}

Checks the signature of a body, the x-hub-signature-256 header (sha256=<hex>), in constant time. Returns :ok, {:error, :no_secret}, or {:error, :invalid}.