2022-10-12 10:22:22 -04:00
|
|
|
"""Git_service."""
|
|
|
|
import os
|
2022-12-06 15:31:03 -05:00
|
|
|
import shutil
|
2022-12-09 15:01:55 -05:00
|
|
|
import subprocess # noqa we need the subprocess module to safely run the git commands
|
2022-12-08 09:25:27 -05:00
|
|
|
import uuid
|
2022-12-08 16:39:23 -05:00
|
|
|
from typing import Optional
|
2022-12-09 15:01:55 -05:00
|
|
|
from typing import Union
|
2022-10-12 10:22:22 -04:00
|
|
|
|
|
|
|
from flask import current_app
|
2022-12-06 15:31:03 -05:00
|
|
|
from flask import g
|
2022-11-18 16:45:44 -05:00
|
|
|
|
2022-12-09 15:01:55 -05:00
|
|
|
from spiffworkflow_backend.config import ConfigurationError
|
2022-10-12 10:22:22 -04:00
|
|
|
from spiffworkflow_backend.models.process_model import ProcessModelInfo
|
|
|
|
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
|
|
|
|
|
|
|
|
2022-12-09 15:01:55 -05:00
|
|
|
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
|
2022-10-12 10:22:22 -04:00
|
|
|
class GitService:
|
|
|
|
"""GitService."""
|
|
|
|
|
2022-12-09 15:01:55 -05:00
|
|
|
@classmethod
|
|
|
|
def get_current_revision(cls) -> str:
|
2022-10-12 10:22:22 -04:00
|
|
|
"""Get_current_revision."""
|
2023-02-16 07:39:40 -05:00
|
|
|
bpmn_spec_absolute_dir = current_app.config[
|
|
|
|
"SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR"
|
|
|
|
]
|
2022-10-12 10:22:22 -04:00
|
|
|
# The value includes a carriage return character at the end, so we don't grab the last character
|
2022-12-09 15:01:55 -05:00
|
|
|
with FileSystemService.cd(bpmn_spec_absolute_dir):
|
|
|
|
return cls.run_shell_command_to_get_stdout(
|
|
|
|
["git", "rev-parse", "--short", "HEAD"]
|
|
|
|
)
|
|
|
|
|
|
|
|
@classmethod
|
2022-10-12 10:22:22 -04:00
|
|
|
def get_instance_file_contents_for_revision(
|
2022-12-12 15:36:03 -05:00
|
|
|
cls,
|
|
|
|
process_model: ProcessModelInfo,
|
|
|
|
revision: str,
|
|
|
|
file_name: Optional[str] = None,
|
2022-12-09 15:01:55 -05:00
|
|
|
) -> str:
|
2022-10-12 10:22:22 -04:00
|
|
|
"""Get_instance_file_contents_for_revision."""
|
2023-02-16 07:39:40 -05:00
|
|
|
bpmn_spec_absolute_dir = current_app.config[
|
|
|
|
"SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR"
|
|
|
|
]
|
2022-10-12 10:22:22 -04:00
|
|
|
process_model_relative_path = FileSystemService.process_model_relative_path(
|
|
|
|
process_model
|
|
|
|
)
|
2022-12-12 15:08:09 -05:00
|
|
|
file_name_to_use = file_name
|
|
|
|
if file_name_to_use is None:
|
|
|
|
file_name_to_use = process_model.primary_file_name
|
2022-12-09 15:01:55 -05:00
|
|
|
with FileSystemService.cd(bpmn_spec_absolute_dir):
|
|
|
|
shell_command = [
|
|
|
|
"git",
|
|
|
|
"show",
|
2022-12-12 15:08:09 -05:00
|
|
|
f"{revision}:{process_model_relative_path}/{file_name_to_use}",
|
2022-12-09 15:01:55 -05:00
|
|
|
]
|
|
|
|
return cls.run_shell_command_to_get_stdout(shell_command)
|
|
|
|
|
|
|
|
@classmethod
|
2022-12-15 12:52:53 -05:00
|
|
|
def commit(
|
|
|
|
cls,
|
|
|
|
message: str,
|
|
|
|
repo_path: Optional[str] = None,
|
|
|
|
branch_name: Optional[str] = None,
|
|
|
|
) -> str:
|
2022-10-12 10:22:22 -04:00
|
|
|
"""Commit."""
|
2022-12-15 12:52:53 -05:00
|
|
|
cls.check_for_basic_configs()
|
|
|
|
branch_name_to_use = branch_name
|
|
|
|
if branch_name_to_use is None:
|
2023-02-15 17:07:12 -05:00
|
|
|
branch_name_to_use = current_app.config["SPIFFWORKFLOW_BACKEND_GIT_BRANCH"]
|
2022-12-08 16:39:23 -05:00
|
|
|
repo_path_to_use = repo_path
|
|
|
|
if repo_path is None:
|
2023-02-16 07:39:40 -05:00
|
|
|
repo_path_to_use = current_app.config[
|
|
|
|
"SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR"
|
|
|
|
]
|
2022-12-09 15:01:55 -05:00
|
|
|
if repo_path_to_use is None:
|
2023-02-16 07:39:40 -05:00
|
|
|
raise ConfigurationError(
|
|
|
|
"SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR config must be set"
|
|
|
|
)
|
2023-02-15 17:07:12 -05:00
|
|
|
if current_app.config["SPIFFWORKFLOW_BACKEND_GIT_SSH_PRIVATE_KEY"]:
|
2023-02-16 07:39:40 -05:00
|
|
|
os.environ["SPIFFWORKFLOW_BACKEND_GIT_SSH_PRIVATE_KEY"] = (
|
|
|
|
current_app.config["SPIFFWORKFLOW_BACKEND_GIT_SSH_PRIVATE_KEY"]
|
|
|
|
)
|
2022-12-08 16:39:23 -05:00
|
|
|
|
2022-10-12 10:22:22 -04:00
|
|
|
git_username = ""
|
|
|
|
git_email = ""
|
2023-02-16 07:39:40 -05:00
|
|
|
if (
|
|
|
|
current_app.config["SPIFFWORKFLOW_BACKEND_GIT_USERNAME"]
|
|
|
|
and current_app.config["SPIFFWORKFLOW_BACKEND_GIT_USER_EMAIL"]
|
|
|
|
):
|
2023-02-15 17:07:12 -05:00
|
|
|
git_username = current_app.config["SPIFFWORKFLOW_BACKEND_GIT_USERNAME"]
|
|
|
|
git_email = current_app.config["SPIFFWORKFLOW_BACKEND_GIT_USER_EMAIL"]
|
2022-12-08 16:39:23 -05:00
|
|
|
shell_command_path = os.path.join(
|
|
|
|
current_app.root_path, "..", "..", "bin", "git_commit_bpmn_models_repo"
|
|
|
|
)
|
2022-12-09 15:01:55 -05:00
|
|
|
shell_command = [
|
|
|
|
shell_command_path,
|
|
|
|
repo_path_to_use,
|
|
|
|
message,
|
2022-12-15 12:52:53 -05:00
|
|
|
branch_name_to_use,
|
2022-12-09 15:01:55 -05:00
|
|
|
git_username,
|
|
|
|
git_email,
|
2023-02-15 17:17:47 -05:00
|
|
|
current_app.config["SPIFFWORKFLOW_BACKEND_GIT_USER_PASSWORD"],
|
2022-12-09 15:01:55 -05:00
|
|
|
]
|
|
|
|
return cls.run_shell_command_to_get_stdout(shell_command)
|
|
|
|
|
|
|
|
@classmethod
|
2022-12-15 12:52:53 -05:00
|
|
|
def check_for_basic_configs(cls) -> None:
|
|
|
|
"""Check_for_basic_configs."""
|
2023-02-15 17:07:12 -05:00
|
|
|
if current_app.config["SPIFFWORKFLOW_BACKEND_GIT_BRANCH"] is None:
|
2022-12-15 12:52:53 -05:00
|
|
|
raise MissingGitConfigsError(
|
2023-02-15 17:07:12 -05:00
|
|
|
"Missing config for SPIFFWORKFLOW_BACKEND_GIT_BRANCH. "
|
2022-12-15 12:52:53 -05:00
|
|
|
"This is required for publishing process models"
|
|
|
|
)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def check_for_publish_configs(cls) -> None:
|
2022-12-09 15:01:55 -05:00
|
|
|
"""Check_for_configs."""
|
2022-12-15 12:52:53 -05:00
|
|
|
cls.check_for_basic_configs()
|
2023-02-16 13:47:26 -05:00
|
|
|
if current_app.config["SPIFFWORKFLOW_BACKEND_GIT_PUBLISH_TARGET_BRANCH"] is None:
|
2022-12-09 15:01:55 -05:00
|
|
|
raise MissingGitConfigsError(
|
2023-02-16 13:47:26 -05:00
|
|
|
"Missing config for SPIFFWORKFLOW_BACKEND_GIT_PUBLISH_TARGET_BRANCH. "
|
2022-12-09 15:01:55 -05:00
|
|
|
"This is required for publishing process models"
|
|
|
|
)
|
2023-02-16 07:39:40 -05:00
|
|
|
if (
|
2023-02-16 13:47:26 -05:00
|
|
|
current_app.config["SPIFFWORKFLOW_BACKEND_GIT_PUBLISH_CLONE_URL"]
|
2023-02-16 07:39:40 -05:00
|
|
|
is None
|
|
|
|
):
|
2022-12-09 15:01:55 -05:00
|
|
|
raise MissingGitConfigsError(
|
2023-02-16 13:47:26 -05:00
|
|
|
"Missing config for SPIFFWORKFLOW_BACKEND_GIT_PUBLISH_CLONE_URL."
|
2023-02-16 07:39:40 -05:00
|
|
|
" This is required for publishing process models"
|
2022-12-09 15:01:55 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
@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
|
2022-12-12 10:05:08 -05:00
|
|
|
return result.stdout.decode("utf-8").strip()
|
2022-12-09 15:01:55 -05:00
|
|
|
|
|
|
|
@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
|
2022-12-09 16:51:00 -05:00
|
|
|
def handle_web_hook(cls, webhook: dict) -> bool:
|
2022-12-09 15:01:55 -05:00
|
|
|
"""Handle_web_hook."""
|
2022-12-15 12:52:53 -05:00
|
|
|
cls.check_for_publish_configs()
|
2022-12-09 15:01:55 -05:00
|
|
|
|
|
|
|
if "repository" not in webhook or "clone_url" not in webhook["repository"]:
|
|
|
|
raise InvalidGitWebhookBodyError(
|
2022-12-30 23:08:00 -05:00
|
|
|
"Cannot find required keys of 'repository:clone_url' from webhook"
|
|
|
|
f" body: {webhook}"
|
2022-12-09 15:01:55 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
clone_url = webhook["repository"]["clone_url"]
|
2023-02-16 07:39:40 -05:00
|
|
|
if (
|
|
|
|
clone_url
|
2023-02-16 13:47:26 -05:00
|
|
|
!= current_app.config["SPIFFWORKFLOW_BACKEND_GIT_PUBLISH_CLONE_URL"]
|
2023-02-16 07:39:40 -05:00
|
|
|
):
|
2022-12-09 15:01:55 -05:00
|
|
|
raise GitCloneUrlMismatchError(
|
2022-12-30 23:08:00 -05:00
|
|
|
"Configured clone url does not match clone url from webhook:"
|
|
|
|
f" {clone_url}"
|
2022-12-09 15:01:55 -05:00
|
|
|
)
|
|
|
|
|
2022-12-09 16:51:00 -05:00
|
|
|
if "ref" not in webhook:
|
|
|
|
raise InvalidGitWebhookBodyError(
|
|
|
|
f"Could not find the 'ref' arg in the webhook boy: {webhook}"
|
|
|
|
)
|
|
|
|
|
2023-02-15 17:07:12 -05:00
|
|
|
if current_app.config["SPIFFWORKFLOW_BACKEND_GIT_BRANCH"] is None:
|
2022-12-09 16:51:00 -05:00
|
|
|
raise MissingGitConfigsError(
|
2023-02-16 07:39:40 -05:00
|
|
|
"Missing config for SPIFFWORKFLOW_BACKEND_GIT_BRANCH. This is required"
|
|
|
|
" for updating the repository as a result of the webhook"
|
2022-12-09 16:51:00 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
ref = webhook["ref"]
|
2023-02-15 17:07:12 -05:00
|
|
|
git_branch = current_app.config["SPIFFWORKFLOW_BACKEND_GIT_BRANCH"]
|
2022-12-09 16:51:00 -05:00
|
|
|
if ref != f"refs/heads/{git_branch}":
|
|
|
|
return False
|
|
|
|
|
2023-02-16 07:39:40 -05:00
|
|
|
with FileSystemService.cd(
|
|
|
|
current_app.config["SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR"]
|
|
|
|
):
|
2022-12-09 15:01:55 -05:00
|
|
|
cls.run_shell_command(["git", "pull"])
|
2022-12-09 16:51:00 -05:00
|
|
|
return True
|
2022-12-06 15:31:03 -05:00
|
|
|
|
2022-12-08 16:39:23 -05:00
|
|
|
@classmethod
|
|
|
|
def publish(cls, process_model_id: str, branch_to_update: str) -> str:
|
|
|
|
"""Publish."""
|
2022-12-15 12:52:53 -05:00
|
|
|
cls.check_for_publish_configs()
|
2022-12-06 15:31:03 -05:00
|
|
|
source_process_model_root = FileSystemService.root_path()
|
2022-12-08 16:39:23 -05:00
|
|
|
source_process_model_path = os.path.join(
|
|
|
|
source_process_model_root, process_model_id
|
|
|
|
)
|
2022-12-08 09:25:27 -05:00
|
|
|
unique_hex = uuid.uuid4().hex
|
|
|
|
clone_dir = f"sample-process-models.{unique_hex}"
|
2022-12-06 15:31:03 -05:00
|
|
|
|
|
|
|
# clone new instance of sample-process-models, checkout branch_to_update
|
2022-12-08 17:12:19 -05:00
|
|
|
# we are adding a guid to this so the flake8 issue has been mitigated
|
|
|
|
destination_process_root = f"/tmp/{clone_dir}" # noqa
|
|
|
|
|
2023-02-16 07:39:40 -05:00
|
|
|
git_clone_url = current_app.config[
|
2023-02-16 13:47:26 -05:00
|
|
|
"SPIFFWORKFLOW_BACKEND_GIT_PUBLISH_CLONE_URL"
|
2023-02-16 07:39:40 -05:00
|
|
|
]
|
2023-01-21 20:51:11 -05:00
|
|
|
if git_clone_url.startswith("https://"):
|
2023-01-20 15:11:23 -05:00
|
|
|
git_clone_url = git_clone_url.replace(
|
|
|
|
"https://",
|
2023-02-15 17:17:47 -05:00
|
|
|
f"https://{current_app.config['SPIFFWORKFLOW_BACKEND_GIT_USERNAME']}:{current_app.config['SPIFFWORKFLOW_BACKEND_GIT_USER_PASSWORD']}@",
|
2023-01-20 15:11:23 -05:00
|
|
|
)
|
2022-12-09 15:01:55 -05:00
|
|
|
cmd = ["git", "clone", git_clone_url, destination_process_root]
|
|
|
|
|
|
|
|
cls.run_shell_command(cmd)
|
2022-12-08 16:39:23 -05:00
|
|
|
with FileSystemService.cd(destination_process_root):
|
|
|
|
# create publish branch from branch_to_update
|
2022-12-09 15:01:55 -05:00
|
|
|
cls.run_shell_command(["git", "checkout", branch_to_update])
|
|
|
|
branch_to_pull_request = f"publish-{process_model_id}"
|
|
|
|
|
|
|
|
# check if branch exists and checkout appropriately
|
|
|
|
command = [
|
|
|
|
"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])
|
2022-12-08 16:39:23 -05:00
|
|
|
else:
|
2022-12-09 15:01:55 -05:00
|
|
|
cls.run_shell_command(["git", "checkout", "-b", branch_to_pull_request])
|
2022-12-08 16:39:23 -05:00
|
|
|
|
|
|
|
# copy files from process model into the new publish branch
|
|
|
|
destination_process_model_path = os.path.join(
|
|
|
|
destination_process_root, process_model_id
|
|
|
|
)
|
|
|
|
if os.path.exists(destination_process_model_path):
|
|
|
|
shutil.rmtree(destination_process_model_path)
|
|
|
|
shutil.copytree(source_process_model_path, destination_process_model_path)
|
|
|
|
|
2022-12-09 15:01:55 -05:00
|
|
|
# add and commit files to branch_to_pull_request, then push
|
|
|
|
commit_message = (
|
|
|
|
f"Request to publish changes to {process_model_id}, "
|
|
|
|
f"from {g.user.username} on {current_app.config['ENV_IDENTIFIER']}"
|
|
|
|
)
|
2022-12-15 12:52:53 -05:00
|
|
|
cls.commit(commit_message, destination_process_root, branch_to_pull_request)
|
2022-12-08 16:39:23 -05:00
|
|
|
|
|
|
|
# build url for github page to open PR
|
2022-12-09 15:01:55 -05:00
|
|
|
git_remote = cls.run_shell_command_to_get_stdout(
|
|
|
|
["git", "config", "--get", "remote.origin.url"]
|
|
|
|
)
|
|
|
|
remote_url = git_remote.strip().replace(".git", "")
|
|
|
|
pr_url = f"{remote_url}/compare/{branch_to_update}...{branch_to_pull_request}?expand=1"
|
2022-12-08 09:26:10 -05:00
|
|
|
|
|
|
|
# try to clean up
|
|
|
|
if os.path.exists(destination_process_root):
|
|
|
|
shutil.rmtree(destination_process_root)
|
|
|
|
|
|
|
|
return pr_url
|