snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-15 15:05:30 +05:00
commit 64898b2c79
8999 changed files with 1757358 additions and 0 deletions

View File

View File

@@ -0,0 +1,33 @@
# Zulip Heroku integration
Receive notifications in Zulip whenever a new version of an app
is pushed to Heroku using the Zulip Heroku plugin!
{start_tabs}
1. {!create-an-incoming-webhook.md!}
1. {!generate-webhook-url-basic.md!}
1. From the Heroku dashboard page for your app, click the “More”
menu at the top-right, and select **View Webhooks**.
1. Click **Create Webhook**, and set the **Payload URL** to the URL
generated above. From **Event Types**, select the events that you
want to receive notifications for, and click **Add Webhook**.
{end_tabs}
{!congrats.md!}
![](/static/images/integrations/heroku/001.png)
{!event-filtering-additional-feature.md!}
### Related documentation
{!webhooks-url-specification.md!}
- [Heroku App webhooks documentation][Heroku Webhooks Documentation]
[Heroku Webhooks Documentation]: https://devcenter.heroku.com/articles/app-webhooks

View File

@@ -0,0 +1,55 @@
{
"id": "2d8d723d-1024-42f1-9cd3-ff5799fb7eac",
"data": {
"id": "b6d5ff62-08db-4044-b96c-b8e9e41f1191",
"app": {
"id": "4335bcdb-5f6f-41f8-a31b-84697ec96475",
"name": "Example Project"
},
"slug": null,
"user": {
"id": "9126ad9a-9950-46df-857f-6970ebf8a480",
"email": "bwilliams@example.com"
},
"stack": "heroku-24",
"status": "pending",
"release": null,
"metadata": {},
"buildpacks": [],
"created_at": "2025-05-17T11:37:36Z",
"updated_at": "2025-05-17T11:37:36Z",
"source_blob": {
"url": "https://codeload.github.com/theofficialvedantjoshi/media-recommender/legacy.tar.gz/1a5f89db9a6a62f29689b6eeea65e6b6eb0879cd",
"version": "1a5f89db9a6a62f29689b6eeea65e6b6eb0879cd",
"checksum": null
},
"output_stream_url": "https://build-output.heroku.com/streams/43/4335bcdb-5f6f-41f8-a31b-84697ec96475/logs/b6/b6d5ff62-08db-4044-b96c-b8e9e41f1191.log?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAZSXS6CXK3PQ5Y6GY%2F20250517%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250517T113737Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=b2f0cac0426f4898d9bdcd214401027af128935b9e9618eeb6f83a3546b142d2"
},
"actor": {
"id": "9126ad9a-9950-46df-857f-6970ebf8a480",
"email": "bwilliams@example.com"
},
"action": "create",
"version": "application/vnd.heroku+json; version=3",
"resource": "build",
"sequence": null,
"created_at": "2025-05-17T11:37:37+00:00",
"updated_at": "2025-05-17T11:37:37+00:00",
"published_at": "2025-05-17T11:37:37Z",
"previous_data": {},
"webhook_metadata": {
"attempt": {
"id": "761b853a-315c-43a2-abe5-cbd01bdf6914"
},
"delivery": {
"id": "5483911a-8230-4d10-a835-e7bc07a5fa94"
},
"event": {
"id": "2d8d723d-1024-42f1-9cd3-ff5799fb7eac",
"include": "api:build"
},
"webhook": {
"id": "e1020099-2147-45d9-b7a1-bf1422851eea"
}
}
}

View File

@@ -0,0 +1,66 @@
{
"id": "aab185d0-2611-4f40-817c-caba2ad7d2de",
"data": {
"id": "b6d5ff62-08db-4044-b96c-b8e9e41f1191",
"app": {
"id": "4335bcdb-5f6f-41f8-a31b-84697ec96475",
"name": "Example Project"
},
"slug": {
"id": "e7cb552a-e840-48cb-9164-081d1e6da274",
"commit": "1a5f89db9a6a62f29689b6eeea65e6b6eb0879cd",
"commit_description": ""
},
"user": {
"id": "9126ad9a-9950-46df-857f-6970ebf8a480",
"email": "bwilliams@example.com"
},
"stack": "heroku-24",
"status": "succeeded",
"release": {
"id": "6be9012f-1c42-4f2d-bb99-af152aad07d8",
"version": 3
},
"metadata": {},
"buildpacks": [
{
"url": "https://buildpack-registry.s3.amazonaws.com/buildpacks/heroku/python.tgz"
}
],
"created_at": "2025-05-17T11:37:36Z",
"updated_at": "2025-05-17T11:38:03Z",
"source_blob": {
"url": "https://codeload.github.com/theofficialvedantjoshi/media-recommender/legacy.tar.gz/1a5f89db9a6a62f29689b6eeea65e6b6eb0879cd",
"version": "1a5f89db9a6a62f29689b6eeea65e6b6eb0879cd",
"checksum": null
},
"output_stream_url": "https://build-output.heroku.com/streams/43/4335bcdb-5f6f-41f8-a31b-84697ec96475/logs/b6/b6d5ff62-08db-4044-b96c-b8e9e41f1191.log?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAZSXS6CXK3PQ5Y6GY%2F20250517%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250517T113803Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=0aa7c17cbf2fb161feec2da740e6956a39c25b8c55fbce6ebec5b6d4e01ade7c"
},
"actor": {
"id": "9126ad9a-9950-46df-857f-6970ebf8a480",
"email": "bwilliams@example.com"
},
"action": "update",
"version": "application/vnd.heroku+json; version=3",
"resource": "build",
"sequence": null,
"created_at": "2025-05-17T11:38:03+00:00",
"updated_at": "2025-05-17T11:38:03+00:00",
"published_at": "2025-05-17T11:38:04Z",
"previous_data": {},
"webhook_metadata": {
"attempt": {
"id": "94b94f6c-82ff-4c84-a4d9-49def759e768"
},
"delivery": {
"id": "4dad5178-4bf5-4937-82c8-e89a83a499b4"
},
"event": {
"id": "aab185d0-2611-4f40-817c-caba2ad7d2de",
"include": "api:build"
},
"webhook": {
"id": "e1020099-2147-45d9-b7a1-bf1422851eea"
}
}
}

View File

@@ -0,0 +1,64 @@
{
"id": "55fa9c58-68ae-4183-86df-29a7b27cdf84",
"data": {
"id": "6be9012f-1c42-4f2d-bb99-af152aad07d8",
"app": {
"id": "4335bcdb-5f6f-41f8-a31b-84697ec96475",
"name": "Example Project",
"process_tier": "basic"
},
"slug": {
"id": "e7cb552a-e840-48cb-9164-081d1e6da274",
"commit": "1a5f89db9a6a62f29689b6eeea65e6b6eb0879cd",
"commit_description": ""
},
"user": {
"id": "9126ad9a-9950-46df-857f-6970ebf8a480",
"email": "bwilliams@example.com"
},
"stack": "heroku-24",
"status": "succeeded",
"current": true,
"pstable": {},
"version": 3,
"artifacts": [
{
"id": "e7cb552a-e840-48cb-9164-081d1e6da274",
"type": "slug"
}
],
"created_at": "2025-05-17T11:37:58Z",
"updated_at": "2025-05-17T11:37:58Z",
"description": "Deploy 1a5f89db",
"addon_plan_names": [],
"output_stream_url": null,
"eligible_for_rollback": true
},
"actor": {
"id": "9126ad9a-9950-46df-857f-6970ebf8a480",
"email": "bwilliams@example.com"
},
"action": "create",
"version": "application/vnd.heroku+json; version=3",
"resource": "release",
"sequence": null,
"created_at": "2025-05-17T11:37:58.404020Z",
"updated_at": "2025-05-17T11:37:58.404024Z",
"published_at": "2025-05-17T11:37:58Z",
"previous_data": {},
"webhook_metadata": {
"attempt": {
"id": "e1d38e49-586a-4b53-aa5f-ad8abad73ba8"
},
"delivery": {
"id": "7e99cb52-1369-4507-be9e-32309898462d"
},
"event": {
"id": "55fa9c58-68ae-4183-86df-29a7b27cdf84",
"include": "api:release"
},
"webhook": {
"id": "e1020099-2147-45d9-b7a1-bf1422851eea"
}
}
}

View File

@@ -0,0 +1,64 @@
{
"id": "d5d2fb1d-f086-41bf-b054-1d349d4c4ce2",
"data": {
"id": "6be9012f-1c42-4f2d-bb99-af152aad07d8",
"app": {
"id": "4335bcdb-5f6f-41f8-a31b-84697ec96475",
"name": "Example Project",
"process_tier": "basic"
},
"slug": {
"id": "e7cb552a-e840-48cb-9164-081d1e6da274",
"commit": "1a5f89db9a6a62f29689b6eeea65e6b6eb0879cd",
"commit_description": ""
},
"user": {
"id": "9126ad9a-9950-46df-857f-6970ebf8a480",
"email": "bwilliams@example.com"
},
"stack": "heroku-24",
"status": "succeeded",
"current": true,
"pstable": {},
"version": 3,
"artifacts": [
{
"id": "e7cb552a-e840-48cb-9164-081d1e6da274",
"type": "slug"
}
],
"created_at": "2025-05-17T11:37:58Z",
"updated_at": "2025-05-17T11:37:58Z",
"description": "Deploy 1a5f89db",
"addon_plan_names": [],
"output_stream_url": null,
"eligible_for_rollback": true
},
"actor": {
"id": "9126ad9a-9950-46df-857f-6970ebf8a480",
"email": "bwilliams@example.com"
},
"action": "update",
"version": "application/vnd.heroku+json; version=3",
"resource": "release",
"sequence": null,
"created_at": "2025-05-17T11:37:58.562518Z",
"updated_at": "2025-05-17T11:37:58.562524Z",
"published_at": "2025-05-17T11:37:58Z",
"previous_data": {},
"webhook_metadata": {
"attempt": {
"id": "6085f041-b991-4610-bc7f-0af82e04667f"
},
"delivery": {
"id": "09f411b3-fefa-44d1-9508-e3c43e0fd46e"
},
"event": {
"id": "d5d2fb1d-f086-41bf-b054-1d349d4c4ce2",
"include": "api:release"
},
"webhook": {
"id": "e1020099-2147-45d9-b7a1-bf1422851eea"
}
}
}

View File

@@ -0,0 +1,93 @@
import orjson
from zerver.lib.test_classes import WebhookTestCase
from zerver.webhooks.fixtureless_integrations import BO_EMAIL, PROJECT_NAME
from zerver.webhooks.heroku.view import IGNORED_ENTITIES, STATUS_MAP
class HerokuHookTests(WebhookTestCase):
TOPIC_NAME = PROJECT_NAME
def test_build_create(self) -> None:
expected_message = f":time_ticking: {BO_EMAIL} triggered a [build](https://build-output.heroku.com/streams/43/4335bcdb-5f6f-41f8-a31b-84697ec96475/logs/b6/b6d5ff62-08db-4044-b96c-b8e9e41f1191.log?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAZSXS6CXK3PQ5Y6GY%2F20250517%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250517T113737Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=b2f0cac0426f4898d9bdcd214401027af128935b9e9618eeb6f83a3546b142d2)."
self.check_webhook("build_create", self.TOPIC_NAME, expected_message)
def test_build_update(self) -> None:
expected_message = f":check: The [build](https://build-output.heroku.com/streams/43/4335bcdb-5f6f-41f8-a31b-84697ec96475/logs/b6/b6d5ff62-08db-4044-b96c-b8e9e41f1191.log?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAZSXS6CXK3PQ5Y6GY%2F20250517%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250517T113803Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=0aa7c17cbf2fb161feec2da740e6956a39c25b8c55fbce6ebec5b6d4e01ade7c) triggered by {BO_EMAIL} **succeeded**."
self.check_webhook("build_update", self.TOPIC_NAME, expected_message)
def test_ignored_entities(self) -> None:
for entity in IGNORED_ENTITIES:
with self.subTest(entity=entity):
payload = {"resource": entity, "action": "create", "data": {}}
last_message_id = self.get_last_message().id
result = self.client_post(
self.url, orjson.dumps(payload), content_type="application/json"
)
self.assert_json_success(result)
self.assertEqual(self.get_last_message().id, last_message_id)
def test_release_create(self) -> None:
expected_message = f":check: {BO_EMAIL} triggered a release(v3): Deploy 1a5f89db."
self.check_webhook("release_create", self.TOPIC_NAME, expected_message)
def test_release_update(self) -> None:
expected_message = (
f":check: The release(v3): Deploy 1a5f89db triggered by {BO_EMAIL} **succeeded**."
)
self.check_webhook("release_update", self.TOPIC_NAME, expected_message)
def test_release_phase_update(self) -> None:
self.subscribe(self.test_user, self.channel_name)
payload = self.webhook_fixture_data(self.webhook_dir_name, "release_update")
data = orjson.loads(payload)
data["data"]["current"] = False
last_message_id = self.get_last_message().id
result = self.client_post(self.url, orjson.dumps(data), content_type="application/json")
self.assert_json_success(result)
self.assertEqual(self.get_last_message().id, last_message_id)
def test_all_status_emojis(self) -> None:
self.subscribe(self.test_user, self.channel_name)
expected_message_template = "{emoji} The [build](https://build-output.heroku.com/streams/43/4335bcdb-5f6f-41f8-a31b-84697ec96475/logs/b6/b6d5ff62-08db-4044-b96c-b8e9e41f1191.log?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAZSXS6CXK3PQ5Y6GY%2F20250517%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250517T113803Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=0aa7c17cbf2fb161feec2da740e6956a39c25b8c55fbce6ebec5b6d4e01ade7c) triggered by {email} **{status}**."
payload = self.webhook_fixture_data(self.webhook_dir_name, "build_update")
data = orjson.loads(payload)
for status, emoji in STATUS_MAP.items():
with self.subTest(status=status):
data["data"]["status"] = status
expected_message = expected_message_template.format(
emoji=emoji, status=status, email=BO_EMAIL
)
msg = self.send_webhook_payload(
self.test_user,
self.url,
orjson.dumps(data).decode(),
content_type="application/json",
)
self.assert_channel_message(
message=msg,
channel_name=self.channel_name,
topic_name=self.TOPIC_NAME,
content=expected_message,
)
def test_status_not_in_map(self) -> None:
self.subscribe(self.test_user, self.channel_name)
expected_message = f"The [build](https://build-output.heroku.com/streams/43/4335bcdb-5f6f-41f8-a31b-84697ec96475/logs/b6/b6d5ff62-08db-4044-b96c-b8e9e41f1191.log?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAZSXS6CXK3PQ5Y6GY%2F20250517%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250517T113803Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=0aa7c17cbf2fb161feec2da740e6956a39c25b8c55fbce6ebec5b6d4e01ade7c) triggered by {BO_EMAIL} **building**."
payload = self.webhook_fixture_data(self.webhook_dir_name, "build_update")
data = orjson.loads(payload)
data["data"]["status"] = "building"
msg = self.send_webhook_payload(
self.test_user,
self.url,
orjson.dumps(data).decode(),
content_type="application/json",
)
self.assert_channel_message(
message=msg,
channel_name=self.channel_name,
topic_name=self.TOPIC_NAME,
content=expected_message,
)

View File

@@ -0,0 +1,106 @@
from django.http import HttpRequest, HttpResponse
from zerver.decorator import webhook_view
from zerver.lib.exceptions import UnsupportedWebhookEventTypeError
from zerver.lib.response import json_success
from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint
from zerver.lib.validator import WildValue, check_bool, check_int, check_none_or, check_string
from zerver.lib.webhooks.common import check_send_webhook_message
from zerver.models import UserProfile
ENTITY_CREATED_MESSAGE = "{status_emoji}{actor} triggered a {entity}."
ENTITY_UPDATED_MESSAGE = "{status_emoji}The {entity} triggered by {actor} **{status}**."
# The events here are ignored not because they are noisy.
# But we do not have accurate fixtures for them.
# The fixtures documented in https://devcenter.heroku.com/articles/webhook-events
# have been reported to drift from actual payloads.
# Heroku does not provide a free trial account.
IGNORED_ENTITIES = [
"addon-attachment",
"addon",
"app",
"collaborator",
"domain",
"dyno",
"formation",
"sni-endpoint",
]
STATUS_MAP = {
"succeeded": ":check:",
"failed": ":warning:",
"pending": ":time_ticking:",
"expired": ":times_up:",
}
def get_body(payload: WildValue, entity: str) -> str:
data = payload["data"]
actor = payload["actor"]["email"].tame(check_string)
entity_url = data["output_stream_url"].tame(check_none_or(check_string))
display_entity = f"[{entity}]({entity_url})" if entity_url else entity
if entity == "release":
version = data["version"].tame(check_int)
description = data["description"].tame(check_string)
display_entity = f"{display_entity}(v{version}): {description}"
action = payload["action"].tame(check_string)
status = data["status"].tame(check_string)
status_emoji = f"{STATUS_MAP[status]} " if status in STATUS_MAP else ""
template = ENTITY_UPDATED_MESSAGE if action == "update" else ENTITY_CREATED_MESSAGE
body = template.format(
entity=display_entity, actor=actor, status=status, status_emoji=status_emoji
)
return body
def should_ignore_event(entity: str, event_type: str, payload: WildValue) -> bool:
if entity in IGNORED_ENTITIES:
return True
# Ignore release phase update events, for now, to avoid duplicate notifications.
# https://devcenter.heroku.com/articles/release-phase
# 1. Release started: action="create" data.status="pending" data.current=false
# 2. Release phase(s) finished: action="update" data.status="succeeded" data.current=false
# 3. Release live: action="update" data.status="succeeded" data.current=true
status = payload["data"]["status"].tame(check_string)
if (
event_type == "release.update"
and status in {"succeeded", "failed"}
and not payload["data"]["current"].tame(check_bool)
):
return True
return False
ALL_EVENT_TYPES = ["build.create", "build.update", "release.create", "release.update"]
@webhook_view("Heroku", all_event_types=ALL_EVENT_TYPES)
@typed_endpoint
def api_heroku_webhook(
request: HttpRequest,
user_profile: UserProfile,
*,
payload: JsonBodyPayload[WildValue],
) -> HttpResponse:
entity = payload["resource"].tame(check_string)
event = payload["action"].tame(check_string)
event_type = f"{entity}.{event}"
if should_ignore_event(entity, event_type, payload):
return json_success(request)
if event_type not in ALL_EVENT_TYPES:
raise UnsupportedWebhookEventTypeError(event_type)
topic_name = payload["data"]["app"]["name"].tame(check_string)
body = get_body(payload, entity)
check_send_webhook_message(request, user_profile, topic_name, body, event_type)
return json_success(request)