cleaned up the git service and expanded the api git hook w/ burnettk

This commit is contained in:
jasquat 2022-12-09 15:01:55 -05:00
parent 1406190b21
commit 3e5ed42eae
8 changed files with 176 additions and 56 deletions

View File

@ -494,6 +494,11 @@ paths:
post: post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.github_webhook_receive operationId: spiffworkflow_backend.routes.process_api_blueprint.github_webhook_receive
summary: receives push webhooks from github so we can keep our process model repo up to date summary: receives push webhooks from github so we can keep our process model repo up to date
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/ProcessModelCategory"
tags: tags:
- git - git
responses: responses:

View File

@ -27,8 +27,6 @@ CONNECTOR_PROXY_URL = environ.get(
"CONNECTOR_PROXY_URL", default="http://localhost:7004" "CONNECTOR_PROXY_URL", default="http://localhost:7004"
) )
GIT_COMMIT_ON_SAVE = environ.get("GIT_COMMIT_ON_SAVE", default="false") == "true"
# Open ID server # Open ID server
OPEN_ID_SERVER_URL = environ.get( OPEN_ID_SERVER_URL = environ.get(
"OPEN_ID_SERVER_URL", default="http://localhost:7002/realms/spiffworkflow" "OPEN_ID_SERVER_URL", default="http://localhost:7002/realms/spiffworkflow"
@ -63,7 +61,9 @@ SPIFFWORKFLOW_BACKEND_LOG_LEVEL = environ.get(
# When a user clicks on the `Publish` button, this is the default branch this server merges into. # When a user clicks on the `Publish` button, this is the default branch this server merges into.
# I.e., dev server could have `staging` here. Staging server might have `production` here. # I.e., dev server could have `staging` here. Staging server might have `production` here.
GIT_MERGE_BRANCH = environ.get("GIT_MERGE_BRANCH", default="staging") GIT_BRANCH_TO_PUBLISH_TO = environ.get("GIT_BRANCH_TO_PUBLISH_TO", default="staging")
GIT_CLONE_URL_FOR_PUBLISHING = environ.get("GIT_CLONE_URL", default=None)
GIT_COMMIT_ON_SAVE = environ.get("GIT_COMMIT_ON_SAVE", default="false") == "true"
# Datbase Configuration # Datbase Configuration
SPIFF_DATABASE_TYPE = environ.get( SPIFF_DATABASE_TYPE = environ.get(

View File

@ -1,7 +1,7 @@
"""Dev.""" """Dev."""
from os import environ from os import environ
GIT_MERGE_BRANCH = environ.get("GIT_MERGE_BRANCH", default="staging") GIT_BRANCH_TO_PUBLISH_TO = environ.get("GIT_BRANCH_TO_PUBLISH_TO", default="staging")
GIT_USERNAME = environ.get("GIT_USERNAME", default="sartography-automated-committer") GIT_USERNAME = environ.get("GIT_USERNAME", default="sartography-automated-committer")
GIT_USER_EMAIL = environ.get( GIT_USER_EMAIL = environ.get(
"GIT_USER_EMAIL", default="sartography-automated-committer@users.noreply.github.com" "GIT_USER_EMAIL", default="sartography-automated-committer@users.noreply.github.com"

View File

@ -12,3 +12,8 @@ SPIFFWORKFLOW_BACKEND_LOG_LEVEL = environ.get(
RUN_BACKGROUND_SCHEDULER = ( RUN_BACKGROUND_SCHEDULER = (
environ.get("RUN_BACKGROUND_SCHEDULER", default="true") == "true" environ.get("RUN_BACKGROUND_SCHEDULER", default="true") == "true"
) )
GIT_CLONE_URL_FOR_PUBLISHING = environ.get(
"GIT_CLONE_URL", default="https://github.com/sartography/sample-process-models.git"
)
GIT_USERNAME = "sartography-automated-committer"
GIT_USER_EMAIL = f"{GIT_USERNAME}@users.noreply.github.com"

View File

@ -5,8 +5,8 @@ from os import environ
environment_identifier_for_this_config_file_only = environ["SPIFFWORKFLOW_BACKEND_ENV"] environment_identifier_for_this_config_file_only = environ["SPIFFWORKFLOW_BACKEND_ENV"]
GIT_COMMIT_ON_SAVE = True GIT_COMMIT_ON_SAVE = True
GIT_USERNAME = environment_identifier_for_this_config_file_only GIT_USERNAME = "sartography-automated-committer"
GIT_USER_EMAIL = f"{environment_identifier_for_this_config_file_only}@example.com" GIT_USER_EMAIL = f"{GIT_USERNAME}@users.noreply.github.com"
SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get( SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME = environ.get(
"SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME", "SPIFFWORKFLOW_BACKEND_PERMISSIONS_FILE_NAME",
default="terraform_deployed_environment.yml", default="terraform_deployed_environment.yml",
@ -24,3 +24,6 @@ SPIFFWORKFLOW_BACKEND_URL = (
f"https://api.{environment_identifier_for_this_config_file_only}.spiffworkflow.org" f"https://api.{environment_identifier_for_this_config_file_only}.spiffworkflow.org"
) )
CONNECTOR_PROXY_URL = f"https://connector-proxy.{environment_identifier_for_this_config_file_only}.spiffworkflow.org" CONNECTOR_PROXY_URL = f"https://connector-proxy.{environment_identifier_for_this_config_file_only}.spiffworkflow.org"
GIT_CLONE_URL_FOR_PUBLISHING = environ.get(
"GIT_CLONE_URL", default="https://github.com/sartography/sample-process-models.git"
)

View File

@ -93,7 +93,7 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
created_at_in_seconds: int = db.Column(db.Integer) created_at_in_seconds: int = db.Column(db.Integer)
status: str = db.Column(db.String(50)) status: str = db.Column(db.String(50))
bpmn_xml_file_contents: bytes | None = None bpmn_xml_file_contents: str | None = None
bpmn_version_control_type: str = db.Column(db.String(50)) bpmn_version_control_type: str = db.Column(db.String(50))
bpmn_version_control_identifier: str = db.Column(db.String(255)) bpmn_version_control_identifier: str = db.Column(db.String(255))
spiff_step: int = db.Column(db.Integer) spiff_step: int = db.Column(db.Integer)
@ -101,9 +101,6 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
@property @property
def serialized(self) -> dict[str, Any]: def serialized(self) -> dict[str, Any]:
"""Return object data in serializeable format.""" """Return object data in serializeable format."""
local_bpmn_xml_file_contents = ""
if self.bpmn_xml_file_contents:
local_bpmn_xml_file_contents = self.bpmn_xml_file_contents.decode("utf-8")
return { return {
"id": self.id, "id": self.id,
"process_model_identifier": self.process_model_identifier, "process_model_identifier": self.process_model_identifier,
@ -112,7 +109,7 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
"start_in_seconds": self.start_in_seconds, "start_in_seconds": self.start_in_seconds,
"end_in_seconds": self.end_in_seconds, "end_in_seconds": self.end_in_seconds,
"process_initiator_id": self.process_initiator_id, "process_initiator_id": self.process_initiator_id,
"bpmn_xml_file_contents": local_bpmn_xml_file_contents, "bpmn_xml_file_contents": self.bpmn_xml_file_contents,
"bpmn_version_control_identifier": self.bpmn_version_control_identifier, "bpmn_version_control_identifier": self.bpmn_version_control_identifier,
"bpmn_version_control_type": self.bpmn_version_control_type, "bpmn_version_control_type": self.bpmn_version_control_type,
"spiff_step": self.spiff_step, "spiff_step": self.spiff_step,

View File

@ -375,7 +375,7 @@ def process_model_publish(
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Process_model_publish.""" """Process_model_publish."""
if branch_to_update is None: if branch_to_update is None:
branch_to_update = current_app.config["GIT_MERGE_BRANCH"] branch_to_update = current_app.config["GIT_BRANCH_TO_PUBLISH_TO"]
process_model_identifier = un_modify_modified_process_model_id( process_model_identifier = un_modify_modified_process_model_id(
modified_process_model_identifier modified_process_model_identifier
) )
@ -1817,13 +1817,13 @@ def get_spiff_task_from_process_instance(
# sample body: # sample body:
# {'ref': 'refs/heads/main', 'repository': {'name': 'sample-process-models', # {"ref": "refs/heads/main", "repository": {"name": "sample-process-models",
# 'full_name': 'sartography/sample-process-models', 'private': False .... }} # "full_name": "sartography/sample-process-models", "private": False .... }}
# test with: ngrok http 7000 # test with: ngrok http 7000
# where 7000 is the port the app is running on locally # where 7000 is the port the app is running on locally
def github_webhook_receive(body: dict) -> Response: def github_webhook_receive(body: Dict) -> Response:
"""Github_webhook_receive.""" """Github_webhook_receive."""
print(f"body: {body}") GitService.handle_web_hook(body)
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json") return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")

View File

@ -1,54 +1,74 @@
"""Git_service.""" """Git_service."""
import os import os
import shutil import shutil
import subprocess # noqa we need the subprocess module to safely run the git commands
import uuid import uuid
from typing import Optional from typing import Optional
from typing import Union
from flask import current_app from flask import current_app
from flask import g from flask import g
from spiffworkflow_backend.config import ConfigurationError
from spiffworkflow_backend.models.process_model import ProcessModelInfo from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.services.file_system_service import FileSystemService from spiffworkflow_backend.services.file_system_service import FileSystemService
class MissingGitConfigsError(Exception):
"""MissingGitConfigsError."""
class InvalidGitWebhookBodyError(Exception):
"""InvalidGitWebhookBodyError."""
class GitCloneUrlMismatchError(Exception):
"""GitCloneUrlMismatchError."""
class GitCommandError(Exception):
"""GitCommandError."""
# TOOD: check for the existence of git and configs on bootup if publishing is enabled
class GitService: class GitService:
"""GitService.""" """GitService."""
@staticmethod @classmethod
def get_current_revision() -> str: def get_current_revision(cls) -> str:
"""Get_current_revision.""" """Get_current_revision."""
bpmn_spec_absolute_dir = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"] bpmn_spec_absolute_dir = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"]
# The value includes a carriage return character at the end, so we don't grab the last character # The value includes a carriage return character at the end, so we don't grab the last character
current_git_revision = os.popen( # noqa: S605 with FileSystemService.cd(bpmn_spec_absolute_dir):
f"cd {bpmn_spec_absolute_dir} && git rev-parse --short HEAD" return cls.run_shell_command_to_get_stdout(
).read()[ ["git", "rev-parse", "--short", "HEAD"]
:-1 )
] # noqa: S605
return current_git_revision
@staticmethod @classmethod
def get_instance_file_contents_for_revision( def get_instance_file_contents_for_revision(
process_model: ProcessModelInfo, revision: str cls, process_model: ProcessModelInfo, revision: str
) -> bytes: ) -> str:
"""Get_instance_file_contents_for_revision.""" """Get_instance_file_contents_for_revision."""
bpmn_spec_absolute_dir = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"] bpmn_spec_absolute_dir = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"]
process_model_relative_path = FileSystemService.process_model_relative_path( process_model_relative_path = FileSystemService.process_model_relative_path(
process_model process_model
) )
shell_cd_command = f"cd {bpmn_spec_absolute_dir}" with FileSystemService.cd(bpmn_spec_absolute_dir):
shell_git_command = f"git show {revision}:{process_model_relative_path}/{process_model.primary_file_name}" shell_command = [
shell_command = f"{shell_cd_command} && {shell_git_command}" "git",
# git show 78ae5eb:category_number_one/script-task/script-task.bpmn "show",
file_contents: str = os.popen(shell_command).read()[:-1] # noqa: S605 f"{revision}:{process_model_relative_path}/{process_model.primary_file_name}",
assert file_contents # noqa: S101 ]
return file_contents.encode("utf-8") return cls.run_shell_command_to_get_stdout(shell_command)
@staticmethod @classmethod
def commit(message: str, repo_path: Optional[str] = None) -> str: def commit(cls, message: str, repo_path: Optional[str] = None) -> str:
"""Commit.""" """Commit."""
repo_path_to_use = repo_path repo_path_to_use = repo_path
if repo_path is None: if repo_path is None:
repo_path_to_use = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"] repo_path_to_use = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"]
if repo_path_to_use is None:
raise ConfigurationError("BPMN_SPEC_ABSOLUTE_DIR config must be set")
git_username = "" git_username = ""
git_email = "" git_email = ""
@ -58,13 +78,90 @@ class GitService:
shell_command_path = os.path.join( shell_command_path = os.path.join(
current_app.root_path, "..", "..", "bin", "git_commit_bpmn_models_repo" current_app.root_path, "..", "..", "bin", "git_commit_bpmn_models_repo"
) )
shell_command = f"{shell_command_path} '{repo_path_to_use}' '{message}' '{git_username}' '{git_email}'" shell_command = [
output = os.popen(shell_command).read() # noqa: S605 shell_command_path,
return output repo_path_to_use,
message,
git_username,
git_email,
]
return cls.run_shell_command_to_get_stdout(shell_command)
@classmethod
def check_for_configs(cls) -> None:
"""Check_for_configs."""
if current_app.config["GIT_BRANCH_TO_PUBLISH_TO"] is None:
raise MissingGitConfigsError(
"Missing config for GIT_BRANCH_TO_PUBLISH_TO. "
"This is required for publishing process models"
)
if current_app.config["GIT_CLONE_URL_FOR_PUBLISHING"] is None:
raise MissingGitConfigsError(
"Missing config for GIT_CLONE_URL_FOR_PUBLISHING. "
"This is required for publishing process models"
)
@classmethod
def run_shell_command_as_boolean(cls, command: list[str]) -> bool:
"""Run_shell_command_as_boolean."""
# we know result will be a bool here
result: bool = cls.run_shell_command(command, return_success_state=True) # type: ignore
return result
@classmethod
def run_shell_command_to_get_stdout(cls, command: list[str]) -> str:
"""Run_shell_command_to_get_stdout."""
# we know result will be a CompletedProcess here
result: subprocess.CompletedProcess[bytes] = cls.run_shell_command(
command, return_success_state=False
) # type: ignore
return result.stdout.decode("utf-8")
@classmethod
def run_shell_command(
cls, command: list[str], return_success_state: bool = False
) -> Union[subprocess.CompletedProcess[bytes], bool]:
"""Run_shell_command."""
# this is fine since we pass the commands directly
result = subprocess.run(command, check=False, capture_output=True) # noqa
if return_success_state:
return result.returncode == 0
if result.returncode != 0:
stdout = result.stdout.decode("utf-8")
stderr = result.stderr.decode("utf-8")
raise GitCommandError(
f"Failed to execute git command: {command} "
f"Stdout: {stdout} "
f"Stderr: {stderr} "
)
return result
# only supports github right now
@classmethod
def handle_web_hook(cls, webhook: dict) -> None:
"""Handle_web_hook."""
cls.check_for_configs()
if "repository" not in webhook or "clone_url" not in webhook["repository"]:
raise InvalidGitWebhookBodyError(
f"Cannot find required keys of 'repository:clone_url' from webhook body: {webhook}"
)
clone_url = webhook["repository"]["clone_url"]
if clone_url != current_app.config["GIT_CLONE_URL_FOR_PUBLISHING"]:
raise GitCloneUrlMismatchError(
f"Configured clone url does not match clone url from webhook: {clone_url}"
)
with FileSystemService.cd(current_app.config["BPMN_SPEC_ABSOLUTE_DIR"]):
cls.run_shell_command(["git", "pull"])
@classmethod @classmethod
def publish(cls, process_model_id: str, branch_to_update: str) -> str: def publish(cls, process_model_id: str, branch_to_update: str) -> str:
"""Publish.""" """Publish."""
cls.check_for_configs()
source_process_model_root = FileSystemService.root_path() source_process_model_root = FileSystemService.root_path()
source_process_model_path = os.path.join( source_process_model_path = os.path.join(
source_process_model_root, process_model_id source_process_model_root, process_model_id
@ -76,21 +173,29 @@ class GitService:
# we are adding a guid to this so the flake8 issue has been mitigated # we are adding a guid to this so the flake8 issue has been mitigated
destination_process_root = f"/tmp/{clone_dir}" # noqa destination_process_root = f"/tmp/{clone_dir}" # noqa
cmd = ( git_clone_url = current_app.config["GIT_CLONE_URL_FOR_PUBLISHING"].replace(
f"git clone https://{current_app.config['GIT_USERNAME']}:{current_app.config['GIT_USER_PASSWORD']}" "https://",
f"@github.com/sartography/sample-process-models.git {destination_process_root}" f"https://{current_app.config['GIT_USERNAME']}:{current_app.config['GIT_USER_PASSWORD']}@",
) )
os.system(cmd) # noqa: S605 cmd = ["git", "clone", git_clone_url, destination_process_root]
cls.run_shell_command(cmd)
with FileSystemService.cd(destination_process_root): with FileSystemService.cd(destination_process_root):
# create publish branch from branch_to_update # create publish branch from branch_to_update
os.system(f"git checkout {branch_to_update}") # noqa: S605 cls.run_shell_command(["git", "checkout", branch_to_update])
publish_branch = f"publish-{process_model_id}" branch_to_pull_request = f"publish-{process_model_id}"
command = f"git show-ref --verify refs/remotes/origin/{publish_branch}"
output = os.popen(command).read() # noqa: S605 # check if branch exists and checkout appropriately
if output: command = [
os.system(f"git checkout {publish_branch}") # noqa: S605 "git",
"show-ref",
"--verify",
f"refs/remotes/origin/{branch_to_pull_request}",
]
if cls.run_shell_command_as_boolean(command):
cls.run_shell_command(["git", "checkout", branch_to_pull_request])
else: else:
os.system(f"git checkout -b {publish_branch}") # noqa: S605 cls.run_shell_command(["git", "checkout", "-b", branch_to_pull_request])
# copy files from process model into the new publish branch # copy files from process model into the new publish branch
destination_process_model_path = os.path.join( destination_process_model_path = os.path.join(
@ -100,15 +205,20 @@ class GitService:
shutil.rmtree(destination_process_model_path) shutil.rmtree(destination_process_model_path)
shutil.copytree(source_process_model_path, destination_process_model_path) shutil.copytree(source_process_model_path, destination_process_model_path)
# add and commit files to publish_branch, then push # add and commit files to branch_to_pull_request, then push
commit_message = f"Request to publish changes to {process_model_id}, from {g.user.username}" commit_message = (
f"Request to publish changes to {process_model_id}, "
f"from {g.user.username} on {current_app.config['ENV_IDENTIFIER']}"
)
cls.commit(commit_message, destination_process_root) cls.commit(commit_message, destination_process_root)
os.system("git push") # noqa cls.run_shell_command(["git", "push"])
# build url for github page to open PR # build url for github page to open PR
output = os.popen("git config --get remote.origin.url").read() # noqa git_remote = cls.run_shell_command_to_get_stdout(
remote_url = output.strip().replace(".git", "") ["git", "config", "--get", "remote.origin.url"]
pr_url = f"{remote_url}/compare/{publish_branch}?expand=1" )
remote_url = git_remote.strip().replace(".git", "")
pr_url = f"{remote_url}/compare/{branch_to_update}...{branch_to_pull_request}?expand=1"
# try to clean up # try to clean up
if os.path.exists(destination_process_root): if os.path.exists(destination_process_root):