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
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:
81
tools/oneclickapps/README.md
Normal file
81
tools/oneclickapps/README.md
Normal 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.
|
||||
0
tools/oneclickapps/__init__.py
Normal file
0
tools/oneclickapps/__init__.py
Normal 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."
|
||||
)
|
||||
Reference in New Issue
Block a user