blob: 1547b0ad3fcc8ffb7bf5870ee611905678bd05ad [file]
.. Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you 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.
.. _howto/webhooks:github:
Inbound Webhooks
================
|experimental|
The ``github`` webhook source type lets Dags run on events from GitHub.com and GitHub Enterprise
Server, such as a label added to a pull request. It is implemented by
:class:`~airflow.providers.github.webhooks.github.GitHubWebhookSource`. Airflow verifies each delivery
with the ``X-Hub-Signature-256`` signature that GitHub computes with the webhook's secret, and records
an event on each of the delivery's topic names that a Dag schedules on.
Inbound webhooks need Airflow 3.5 or later.
Connecting GitHub
-----------------
1. Serve inbound webhooks. They are opt-in: only an API server started with ``webhooks`` in its
``--apps``, such as ``airflow api-server --apps webhooks``, serves ``/webhooks/``, and any other
answers 404. GitHub must reach that server over HTTPS. The inbound webhooks page of the Airflow
documentation describes the app and the ``[webhooks]`` options.
2. Add ``github`` to the ``[webhooks] names`` option.
3. Create the connection ``webhook_github``, of the Inbound Webhook type from the standard provider,
with a new secret as its password:
.. code-block:: bash
SECRET="$(openssl rand -hex 32)" && echo "$SECRET"
airflow connections add webhook_github --conn-type inbound_webhook --conn-password "$SECRET"
The source type defaults to the webhook's name. A second GitHub instance is a second webhook, such
as ``ghes``, whose connection ``webhook_ghes`` has the extra ``{"source_type": "github"}``.
4. In GitHub, open the repository's **Settings**, then **Webhooks**, then **Add webhook**. To cover
every repository of an organization, use the organization's settings instead. Set:
* **Payload URL**: ``https://<your-airflow>/webhooks/github``.
* **Content type**: ``application/json``. The default, ``application/x-www-form-urlencoded``, is
refused.
* **Secret**: the secret from step 3.
* The events your Dags need, such as pull requests or pushes.
GitHub sends a ``ping`` when the webhook is created. Airflow answers it with 200 once its signature is
verified, and records nothing, so the webhook's **Recent Deliveries** tab shows whether the URL and
the secret work.
Topic names
-----------
A delivery to the webhook ``github`` offers names of the form
``github/<path>/<event>/<action>/<detail>``: every prefix of its event, action and detail, each with its
path and with ``*`` in place of each trailing part of the path, so ``*`` stands for anything below that
point.
.. list-table::
:header-rows: 1
:widths: 15 85
* - Part
- Value
* - path
- The repository's full name, such as ``my-org/web``, or ``my-org/*`` for an organization event
that concerns no repository. Names are case-sensitive, and the path is the repository's
``full_name`` exactly as GitHub sends it: ``path="my-org/web"`` does not match a repository that
GitHub names ``My-Org/Web``.
* - event
- The ``X-GitHub-Event`` header, such as ``pull_request`` or ``push``.
* - action
- The payload's ``action``, such as ``labeled``. A delivery without one, such as a push, has its
detail in this place.
* - detail
- The label for ``labeled`` and ``unlabeled``, the branch for ``push`` (tag pushes have none),
and the workflow name for a ``workflow_run`` of a commit of the repository itself. A run for a
pull request from a fork has no detail, because the fork chooses its workflow's name (see
`Security`_).
A value with a character outside ASCII is never part of a name: a push to a branch named ``maín`` offers
``github/my-org/web/push`` and its wildcard paths, but no name for its branch, so it cannot be taken for a
push to ``main``.
A label ``deploy-preview`` added to a pull request in ``my-org/web`` offers nine names, from broad to
specific:
.. code-block:: text
github/my-org/web/pull_request any pull request activity in my-org/web
github/my-org/*/pull_request ... in any repository of my-org
github/*/pull_request ... in any repository
github/my-org/web/pull_request/labeled any label added
github/my-org/*/pull_request/labeled
github/*/pull_request/labeled
github/my-org/web/pull_request/labeled/deploy-preview the deploy-preview label added
github/my-org/*/pull_request/labeled/deploy-preview
github/*/pull_request/labeled/deploy-preview
A push to the branch ``main`` of ``my-org/web`` offers six:
.. code-block:: text
github/my-org/web/push any push to my-org/web
github/my-org/*/push ... to any repository of my-org
github/*/push ... to any repository
github/my-org/web/push/main a push to main
github/my-org/*/push/main
github/*/push/main
A Dag subscribes to a name by scheduling on a ``WebhookTopic``, and finds the payload in each
event's extra:
.. code-block:: python
from airflow.sdk import WebhookTopic, dag, task
preview_requested = WebhookTopic(
webhook="github",
path="my-org/web",
event="pull_request",
action="labeled",
detail="deploy-preview",
)
@dag(schedule=preview_requested)
def deploy_preview():
@task
def deploy(*, triggering_asset_events):
for event in triggering_asset_events[preview_requested]:
if (payload := event.extra.get("payload")) is None:
print("No payload kept for delivery", event.extra["delivery_id"])
continue
print("Pull request", payload["pull_request"]["number"])
deploy()
deploy_preview()
An event's ``payload`` is null when ``store_payload`` keeps no payload, and when what it keeps is
larger than ``[webhooks] max_event_payload_bytes`` (64 KiB by default) as JSON, in which case the
extra also has ``payload_truncated`` set to true. Pushes and pull requests can be larger than that;
``store_payload`` set to ``summary`` (see below) makes them smaller.
As a push has no action, its branch takes the action's place: pushes to ``main`` are
``WebhookTopic(webhook="github", path="my-org/web", event="push", action="main")``.
Attributes
----------
Each delivery record also keeps the delivery's attributes, as far as the delivery has them:
* ``event``: the ``X-GitHub-Event`` header.
* ``action``: the payload's ``action``.
* ``repository``: the repository's full name, such as ``my-org/web``.
* ``organization``: the organization's login.
* ``label``: the name of the payload's label.
* ``branch``:
* for ``pull_request``, ``pull_request_review``, ``pull_request_review_comment`` and
``pull_request_review_thread``, the pull request's head branch, the branch it merges from, which
for a pull request from a fork is a branch of the fork;
* for ``workflow_job`` and ``check_suite``, their ``head_branch``, for ``check_run``, its check
suite's, and for a ``workflow_run`` of a commit of the repository itself, its ``head_branch``;
* for ``create`` and ``delete``, the ``ref`` if ``ref_type`` is ``branch``;
* for any other event, such as ``push`` and ``workflow_dispatch``, the branch that a ``ref`` of the
form ``refs/heads/<branch>`` names. Tags have no branch.
* ``number``: the number of the pull request or issue.
Stored payload
--------------
What is kept of a payload, in the delivery record and in the extra of topic events, is set by
``store_payload`` in the connection's extra, or else by the ``[webhooks] store_payload`` option, whose
default keeps the whole payload. GitHub payloads carry personal data, such as the name and email
address of each commit's author in a push, and a kept payload can be read by every user allowed to read
assets and by every Dag scheduled on the topic.
With ``store_payload`` set to ``summary``:
* Every ``email`` field is dropped, with the ``name`` beside it. These are the name and email address of
the pusher and of each commit's author and committer; ``sender`` still names the pusher's account, and
``username`` each commit author's and committer's account, if GitHub knows it.
* The ``repository``, ``organization`` and ``sender`` objects, which GitHub repeats in full in its
deliveries, keep only their ``full_name`` or ``login``, as do a pull request's ``user`` and the
``repo`` of its ``head`` and ``base``.
Everything else is kept, including commit messages, which can name people, such as in
``Signed-off-by`` lines. ``store_payload`` set to ``none`` keeps no payload.
Security
--------
GitHub's signature covers the body only. The ``X-GitHub-Event`` and ``X-GitHub-Delivery`` headers are
not signed, and the event in a delivery's names comes from ``X-GitHub-Event``. Airflow drops a repeated
delivery only when both its delivery id and its body are the same, so anyone who obtains one signed
body, from a log or a proxy that records requests for example, can send it again under a new delivery
id and under any event name, and so start the Dags scheduled on the names that this offers. This works
until the secret is changed. Keep delivery bodies and the secret out of logs and proxies, and if a
delivery leaks, change the secret in GitHub and in the ``webhook_github`` connection.
A valid signature shows that GitHub sent a delivery, not that the webhook's owner chose what it
says. Many values in it are chosen by other GitHub users: the head branch, title and body of a pull
request from a fork, issue and comment text, label names that anyone with triage access can apply, and
the names and head branches of workflow runs. A pull request from a fork runs the workflows of its own
commit, so its author can give a workflow any name and make it succeed; that is why such runs offer no
workflow name, but their other values, and those of ``workflow_job``, ``check_suite`` and ``check_run``
deliveries, can still come from a fork. Before a Dag acts on a delivery, such as by deploying the commit
it names, check the values it relies on in the payload, such as the run's ``head_repository`` or the
pull request's ``head.repo``.