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

@@ -0,0 +1,81 @@
# One click app release automation
This directory contains scripts for automating the release of Zulip one click apps.
## DigitalOcean 1-Click Application
`prepare_digital_ocean_one_click_app_release.py` creates the image of DigitalOcean 1-Click
app from the latest Zulip release (fetched from https://download.zulip.com/server/). It will
also create a test droplet from the image and send the image and droplet
details to a pre-configured Zulip stream. Anyone, whose key is added to the
Zulip DigitalOcean team can SSH into the droplet for testing.
### Running as GitHub action
`.github/workflows/update-oneclick-apps.yml` is configured to invoke
`prepare_digital_ocean_one_click_app_release.py` as a GitHub action during each Zulip
server release.
You also need to set the following secrets in your GitHub repository to make the action
work correctly. These secrets are passed as environment variables to the GitHub action.
- `ONE_CLICK_ACTION_DIGITALOCEAN_API_KEY` - DigitalOcean API key used for creating droplets, snapshots etc.
- `ONE_CLICK_ACTION_ZULIP_BOT_API_KEY` - The API key of the Zulip bot used for sending messages.
- `ONE_CLICK_ACTION_ZULIP_BOT_EMAIL` - The email of the Zulip bot.
Also pass the following as environment variables in `.github/workflows/update-oneclick-apps.yml`.
- `PYTHON_DIGITALOCEAN_REQUEST_TIMEOUT_SEC` - This configures the maximum number of seconds
to wait before the requests made by `python-digitalocean` time out. If not configured, it's
common for the requests to take 20+ minutes before getting timed out.
### Verifying the one click app image
- The action will send the image name and test droplet details to the stream configured in the
above steps.
- SSH into the test droplet by following the instructions in the message. In order to do this,
you need to add your SSH key to the team.
- After logging into the test droplet, exit the installer and run the following script.
https://raw.githubusercontent.com/digitalocean/marketplace-partners/master/scripts/99-img-check.sh
This script checks whether the image created is valid. It is also run by the DigitalOcean team
before they approve the image submission in the one click app marketplace.
- If there are no errors (you can ignore most of the warnings), exit the SSH connection and
reconnect.
- Populate the details asked by the installer and verify that the installer completes successfully.
One error is expected:
```
User root has a populated authorized_keys file in /root/.ssh/authorized_keys
```
The key in that file will be the SSH key you've authenticated with.
If there are other errors see the section below.
- Use the link generated by the installer to create a new Zulip organization. Do some basic
testing like sending a bunch of messages and reloading the webpage.
- If there are no issues, submit the image in the
[DigitalOcean vendor portal](https://marketplace.digitalocean.com/vendorportal). You need to be
added to the vendor portal team for doing this. Ask Tim to add you if required. During the submission,
make sure to update the blog post URL if it's a major release.
- Keep checking the vendor portal for change in status of the submission. DigitalOcean does nominally send
emails when there are updates on the submission, but we have found the emails to not always arrive.
- If there are any issues with submission, rebuild the image by manually invoking the script and
resubmit. The issues we have seen mostly in the past are caused by the dependencies getting outdated
by the time the DigitalOcean team run the checks. In that case you have to just rebuild the image
by invoking the script.
- Delete the test droplet `oneclickapp-{release_version}-test` after you have completed testing
by going to the [DigitalOcean Zulip team account](https://cloud.digitalocean.com/droplets?i=0242e0).
If there are other existing test droplets with the same name format but with older release versions
feel free to delete them as well. These droplets are also tagged with the `github-action` and `temporary`
tags.
**Errors**
If there are any errors while setting up the one click app installer, you have three options
- Include the fix in the Fabric script that setups the installer.
[01-initial-setup](https://raw.githubusercontent.com/zulip/marketplace-partners/master/marketplace_docs/templates/Fabric/scripts/01-initial-setup)
file should be a good place to include the fix. See
[zulip/marketplace-partners#4](https://github.com/zulip/marketplace-partners/pull/4/files) for an
example fix.
- Wait for the next release to fix the error.

View File

View File

@@ -0,0 +1,170 @@
import os
import subprocess
import time
from pathlib import Path
import digitalocean
import zulip
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
manager = digitalocean.Manager(token=os.environ["DIGITALOCEAN_API_KEY"])
# We just temporarily create the client now, to validate that we can
# auth to the server; reusing it after the whole install fails because
# the connection has been half-closed in a way that breaks it.
zulip.Client()
TEST_DROPLET_SUBDOMAIN = "do"
def generate_ssh_keys() -> None:
subprocess.check_call(
["ssh-keygen", "-f", str(Path.home()) + "/.ssh/id_ed25519", "-P", "", "-t", "ed25519"]
)
def get_public_ssh_key() -> str:
try:
with open(str(Path.home()) + "/.ssh/id_ed25519.pub") as f:
return f.read()
except FileNotFoundError:
return ""
def sleep_until_droplet_action_is_completed(
droplet: digitalocean.Droplet, action_type: str
) -> None:
incomplete = True
while incomplete:
for action in droplet.get_actions():
action.load()
print(f"...[{action.type}]: {action.status}")
if action.type == action_type and action.status == "completed":
incomplete = False
break
if incomplete:
time.sleep(5)
# Sometimes the droplet does not yet have an .ip_address value
# (the attribute is None) after .load()ing the droplet. We cannot
# proceed without the IP, so we wait in a loop until the IP is
# returned to us.
while True:
droplet.load()
if droplet.ip_address:
break
time.sleep(5)
def set_api_request_retry_limits(api_object: digitalocean.baseapi.BaseAPI) -> None:
retry = Retry(connect=5, read=5, backoff_factor=0.1)
adapter = HTTPAdapter(max_retries=retry)
api_object._session.mount("https://", adapter)
def create_droplet(
name: str, ssh_keys: list[str], image: str = "ubuntu-22-04-x64"
) -> digitalocean.Droplet:
droplet = digitalocean.Droplet(
token=manager.token,
name=name,
region="nyc3",
size_slug="s-1vcpu-2gb",
image=image,
backups=False,
ssh_keys=ssh_keys,
tags=["github-action", "temporary"],
)
set_api_request_retry_limits(droplet)
droplet.create()
sleep_until_droplet_action_is_completed(droplet, "create")
return droplet
def create_ssh_key(name: str, public_key: str) -> digitalocean.SSHKey:
action_public_ssh_key_object = digitalocean.SSHKey(
name=name, public_key=public_key, token=manager.token
)
set_api_request_retry_limits(action_public_ssh_key_object)
action_public_ssh_key_object.create()
return action_public_ssh_key_object
def create_snapshot(droplet: digitalocean.Droplet, snapshot_name: str) -> None:
droplet.take_snapshot(snapshot_name, power_off=True)
droplet.load()
sleep_until_droplet_action_is_completed(droplet, "snapshot")
def create_dns_records(droplet: digitalocean.Droplet) -> None:
domain = digitalocean.Domain(token=manager.token, name="oneclick.zulip.dev")
set_api_request_retry_limits(domain)
domain.load()
oneclick_test_app_record_names = [TEST_DROPLET_SUBDOMAIN, f"*.{TEST_DROPLET_SUBDOMAIN}"]
for record in domain.get_records():
if (
record.name in oneclick_test_app_record_names
and record.domain == "oneclick.zulip.dev"
and record.type == "A"
):
record.destroy()
domain.load()
for record_name in oneclick_test_app_record_names:
domain.create_new_domain_record(type="A", name=record_name, data=droplet.ip_address)
def setup_one_click_app_installer(droplet: digitalocean.Droplet) -> None:
subprocess.check_call(
[
"fab",
"build_image",
"-H",
droplet.ip_address,
"--keepalive",
"5",
"--connection-attempts",
"10",
],
cwd="marketplace-partners/marketplace_docs/templates/Fabric",
)
def send_message(content: str) -> None:
request = {
"type": "stream",
"to": os.environ["ONE_CLICK_ACTION_STREAM"],
"topic": "digitalocean installer",
"content": content,
}
zulip.Client().send_message(request)
if __name__ == "__main__":
release_version = os.environ["RELEASE_VERSION"]
generate_ssh_keys()
action_public_ssh_key_object = create_ssh_key(
f"oneclickapp-{release_version}-image-generator-public-key", get_public_ssh_key()
)
image_generator_droplet = create_droplet(
f"oneclickapp-{release_version}-image-generator", [action_public_ssh_key_object]
)
setup_one_click_app_installer(image_generator_droplet)
oneclick_image_name = f"oneclickapp-{release_version}"
create_snapshot(image_generator_droplet, oneclick_image_name)
snapshot = image_generator_droplet.get_snapshots()[0]
send_message(f"One click app image `{oneclick_image_name}` created.")
image_generator_droplet.destroy()
action_public_ssh_key_object.destroy()
test_droplet_name = f"oneclickapp-{release_version}-test"
test_droplet = create_droplet(test_droplet_name, manager.get_all_sshkeys(), image=snapshot.id)
create_dns_records(test_droplet)
send_message(
f"Test droplet `{test_droplet_name}` created. SSH as root to {TEST_DROPLET_SUBDOMAIN}.oneclick.zulip.dev for testing."
)