Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

init contract-deployer #2374

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/contract-deployer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info

# Virtual environments
.venv
1 change: 1 addition & 0 deletions apps/contract-deployer/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
36 changes: 36 additions & 0 deletions apps/contract-deployer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Use Node.js as base image since we need both Node.js and Python
FROM node:22-bullseye-slim

RUN ls

# Install Python and other system dependencies
RUN apt-get update && apt-get install -y \
python3 \
python3-pip \
git \
&& rm -rf /var/lib/apt/lists/*

# Install specific pnpm version
RUN npm install -g [email protected]

# Install dependencies and build the monorepo
RUN pnpm install

# Install uv and Python dependencies
FROM python:3.11-slim-bookworm
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

# Install git
RUN apt-get update && apt-get install -y git

# Expose the port Streamlit runs on
EXPOSE 8501

# Expect source code to be mounted to /app
COPY . /app
WORKDIR /app

# Install deps
RUN pwd && ls -la && uv sync

CMD ["uv", "run", "server.py"]
4 changes: 4 additions & 0 deletions apps/contract-deployer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Contract Deployer

Currently only EVM chains are supported.
Install uv, then run the webapp with: `uv run server.py`
17 changes: 17 additions & 0 deletions apps/contract-deployer/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
services:
contract-deployer:
build:
context: ./
dockerfile: ./Dockerfile
ports:
- "8501:8501"
volumes:
- type: bind
source: ./
target: /app
environment:
- STREAMLIT_SERVER_PORT=8501
- STREAMLIT_SERVER_ADDRESS=0.0.0.0
- GITHUB_TOKEN=${GITHUB_TOKEN}
- GIT_BRANCH=contract-deployer
restart: unless-stopped
115 changes: 115 additions & 0 deletions apps/contract-deployer/git_ops.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from pathlib import Path
import tempfile
import os
import logging
import shutil

import pygit2
from github import Auth
from github import Github
from pygit2 import Signature

GIT_BASE_BRANCH = "origin/tb/hackathon"

logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)


class GitOps(object):
def __init__(
self,
github_token: str,
repo_name: str,
commiter_name: str,
commiter_email: str,
branch_name: str = "contract-deployer",
) -> None:
self.github = Github(auth=Auth.Token(github_token))
self.github_token = github_token
self.repo_name = repo_name
self.branch_name = branch_name
self.signature = Signature(commiter_name, commiter_email)
self.auth_callback = pygit2.RemoteCallbacks(
pygit2.UserPass("x-access-token", self.github_token)
)

self.github_repo = None
self.repo = None
self.checkout_path = None

def setup(self) -> None:
self.checkout_path = Path(
tempfile.mkdtemp(prefix="python-crosschain-contract-deployer-")
)
self.github_repo = self.github.get_repo(self.repo_name)

# TODO if the branch doesn't exist, create it
logger.info(
f"Cloning {self.repo_name} into {self.checkout_path} (branch: {self.branch_name})..."
)
self.repo = pygit2.clone_repository(
self.github_repo.clone_url,
self.checkout_path,
checkout_branch=self.branch_name,
callbacks=self.auth_callback,
)

logger.info(f"Merging main branch into {self.branch_name}...")
base_branch = self.repo.lookup_branch(GIT_BASE_BRANCH, pygit2.GIT_BRANCH_REMOTE)
oid = base_branch.target
print(f"Looked up OID {oid}")
self.repo.merge(oid)

def cleanup(self):
return
if self.checkout_path and os.path.exists(self.checkout_path):
shutil.rmtree(self.checkout_path)
logger.info("Deleted temp dir {}".format(self.checkout_path))
self.checkout_path = None

self.github_repo = None
self.repo = None

def commit_and_push(self, commit_message: str) -> None:
self.repo.index.add_all()
self.repo.index.write()

tree = self.repo.index.write_tree()
# TODO we should sign this commit
self.repo.create_commit(
"HEAD",
self.signature,
self.signature,
commit_message,
tree,
[self.repo.head.target],
)
self.repo.remotes[0].push(
[f"refs/heads/{self.branch_name}:refs/heads/{self.branch_name}"],
callbacks=self.auth_callback,
)

def get_diff(self):
self.repo.index.add_all()
self.repo.index.write()
return self.repo.diff(cached=True)

def get_checkout_path(self):
return self.checkout_path

def create_pull_request(self, pr_title: str, pr_body: str | None = None):
return self.github_repo.create_pull(
title=pr_title, body=pr_body, head=self.branch_name, base=GIT_BASE_BRANCH
)

def __enter__(self):
self.setup()
return self

def __exit__(self, exc_type, exc_value, traceback):
self.cleanup()

def __del__(self):
self.cleanup()
12 changes: 12 additions & 0 deletions apps/contract-deployer/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[project]
name = "contract-deployer"
version = "0.1.0"
description = "Webapp utility to deploy Pyth contracts to EVM chains and generate a PR"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"pygit2>=1.17.0",
"pygithub>=2.5.0",
"pyyaml>=6.0.2",
"streamlit>=1.42.0",
]
69 changes: 69 additions & 0 deletions apps/contract-deployer/run_deploy_script.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import os
from pathlib import Path
import subprocess

import streamlit as st


def get_1pw_deployer_private_key():
return "000"


def build_deploy_command(chain_name):
return f"./deploy.sh {chain_name}"


def run_deploy_script(chain_name: str, repo_base_dir: Path):
# Create a placeholder for the output

with st.expander("Deployment logs", expanded=True):
script_logs_div = st.code("")

script_logs = ""
env = os.environ.copy()
env.update({"PK": get_1pw_deployer_private_key()})
deploy_command = build_deploy_command(chain_name)
process = subprocess.Popen(
[
"bash",
"-c",
deploy_command,
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True,
cwd=(repo_base_dir.joinpath("target_chains/ethereum/contracts")).as_posix(),
env=env,
)

script_logs += f"Started deploying in process {process.pid}\n"

# Stream output
while True:
output = process.stdout.readline()
error = process.stderr.readline()
if output:
script_logs += output.strip() + "\n"
if error:
script_logs += error.strip() + "\n"

script_logs_div.code(script_logs)

# Check if process has finished
if process.poll() is not None:
# Dump any remaining output
remaining_output, remaining_error = process.communicate()
if remaining_output:
script_logs += remaining_output.strip() + "\n"
if remaining_error:
script_logs += remaining_error.strip() + "\n"
script_logs_div.code(script_logs)
break

# Get the return code
if process.returncode == 0:
st.success("Deployment script executed successfully!")
else:
st.error("Deployment script failed!")
105 changes: 105 additions & 0 deletions apps/contract-deployer/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from urllib.parse import urlparse
import streamlit as st

from run_deploy_script import run_deploy_script
from git_ops import GitOps
from update_files import update_files
import os

GITHUB_REPO = "pyth-network/pyth-crosschain"
GITHUB_COMMITER_NAME = "contract-deployer"
GITHUB_COMMITER_EMAIL = "[email protected]"

EVM_CHAINS_YAML = "contract_manager/store/chains/EvmChains.yaml"
RECEIVER_CHAINS_JSON = (
"governance/xc_admin/packages/xc_admin_common/src/receiver_chains.json"
)


def validate_inputs(chain_name: str, rpc_url: str) -> bool:
if not chain_name:
st.error("Chain name cannot be empty")
return False

if not rpc_url:
st.error("RPC URL cannot be empty")
return False

# Validate URL format
try:
result = urlparse(rpc_url)
if not all([result.scheme in ("http", "https"), result.netloc]):
st.error("Invalid RPC URL format. Please enter a valid HTTP/HTTPS URL")
return False
except:
st.error("Invalid URL format")
return False

return True


def main():

st.header("EVM Contract Deployer")

with st.form("deploy_config", enter_to_submit=False):
chain_name = st.text_input("Chain Name", placeholder="e.g. Ethereum, Sepolia")
rpc_url = st.text_input("RPC URL", placeholder="https://...")
is_mainnet = st.checkbox("Is Mainnet?", value=False)
submitted = st.form_submit_button("🚀 Deploy Contracts and Generate PR")

if submitted:
try:
if not validate_inputs(chain_name, rpc_url):
return

token = os.environ.get("GITHUB_TOKEN")
repo_name = os.environ.get("GITHUB_REPO", GITHUB_REPO)
branch_name = os.environ["GIT_BRANCH"]
commiter_name = os.environ.get("GITHUB_COMMITER_NAME", GITHUB_COMMITER_NAME)
commiter_email = os.environ.get(
"GITHUB_COMMITER_EMAIL", GITHUB_COMMITER_EMAIL
)

st.write(f"Cloning the repository. Using branch {branch_name}.")
with GitOps(
token, repo_name, commiter_name, commiter_email, branch_name=branch_name
) as git_ops:
evm_chains_path = os.path.join(
git_ops.get_checkout_path(), EVM_CHAINS_YAML
)
receiver_chains_path = os.path.join(
git_ops.get_checkout_path(), RECEIVER_CHAINS_JSON
)
update_files(
evm_chains_path,
receiver_chains_path,
is_mainnet,
chain_name,
rpc_url,
)

# diff = git_ops.get_diff()
# if diff:
# st.write(diff)
# else:
# return None

run_deploy_script(chain_name, repo_base_dir=git_ops.get_checkout_path())
git_ops.commit_and_push(f"Deploy {chain_name} to {rpc_url}")
pr = git_ops.create_pull_request(f"Deploy {chain_name} to {rpc_url}")
st.write(f"Created pull request <a href={pr.url}>{pr.url}</a>")

except Exception as e:
st.error(f"An unexpected error occurred: {repr(e)}")
raise


if __name__ == "__main__":
# Enable running the app without `streamlit run`
# https://github.com/streamlit/streamlit/issues/9450#issuecomment-2386348596
if "__streamlitmagic__" not in locals():
import streamlit.web.bootstrap

streamlit.web.bootstrap.run(__file__, False, [], {})
main()
Loading
Loading