diff --git a/.mega-linter.yml b/.mega-linter.yml index a17e939c..a6b4e31c 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -8,4 +8,5 @@ ENABLE_LINTERS: DISABLE_ERRORS_LINTERS: - MAKEFILE_CHECKMAKE - - REPOSITORY_KICS \ No newline at end of file + - REPOSITORY_KICS + - BASH_SHELLCHECK \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 17fd6408..0dfe6c4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,16 +55,9 @@ RUN apt-get update \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* -ENTRYPOINT [ "/bin/sh", "-c", "python3.8 -m trestlebot \ - --markdown-path=${MARKDOWN_PATH} \ - --assemble-model=${ASSEMBLE_MODEL} \ - --ssp-index-path=${SSP_INDEX_PATH} \ - --commit-message=${COMMIT_MESSAGE} \ - --branch=${BRANCH} \ - --patterns=${PATTERNS} \ - --committer-name=${COMMIT_USER_NAME} \ - --committer-email=${COMMIT_USER_EMAIL} \ - --author-name=${AUTHOR_NAME} \ - --author-email=${AUTHOR_EMAIL} \ - --working-dir=${WORKING_DIR}" ] +COPY ./entrypoint.sh / + +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["python3.8", "-m" , "trestlebot"] diff --git a/action.yml b/action.yml index ff776b2a..91f18509 100644 --- a/action.yml +++ b/action.yml @@ -16,7 +16,7 @@ inputs: commit_message: description: Commit message required: false - default: "chore: sync automatic updates" + default: "Sync automatic updates" branch: description: Git branch name, where changes should be pushed too. Required if Action is used on the `pull_request` event required: false @@ -46,21 +46,17 @@ inputs: required: false default: ${{ github.actor }}@users.noreply.github.com +outputs: + changes: + description: Value is "true" if changes were committed back to the repository. + commit: + description: Full hash of the created commit. Only present if the "changes" output is "true". + runs: using: "docker" image: "Dockerfile" - env: - MARKDOWN_PATH: ${{ inputs.markdown_path }} - ASSEMBLE_MODEL: ${{ inputs.assemble_model }} - SSP_INDEX_PATH: ${{ inputs.ssp_index_path }} - COMMIT_MESSAGE: ${{ inputs.commit_message }} - BRANCH: ${{ inputs.branch }} - PATTERNS: ${{ inputs.file_pattern }} - COMMIT_USER_NAME: ${{ inputs.commit_user_name }} - COMMIT_USER_EMAIL: ${{ inputs.commit_user_email }} - COMMIT_AUTHOR_NAME: ${{ inputs.commit_author_name }} - COMMIT_AUTHOR_EMAIL: ${{ inputs.commit_author_email }} - REPOSITORY: ${{ inputs.repository }} + entrypoint: [ "/entrypoint.sh"] + branding: icon: "check" color: "green" diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 00000000..f673a9a5 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -eu + +output=$(python3.8 -m trestlebot \ + --markdown-path="${INPUT_MARKDOWN_PATH}" \ + --assemble-model="${INPUT_ASSEMBLE_MODEL}" \ + --ssp-index-path="${INPUT_SSP_INDEX_PATH}" \ + --commit-message="${INPUT_COMMIT_MESSAGE}" \ + --branch="${INPUT_BRANCH}" \ + --patterns="${INPUT_FILE_PATTERN}" \ + --committer-name="${INPUT_COMMIT_USER_NAME}" \ + --committer-email="${INPUT_COMMIT_USER_EMAIL}" \ + --author-name="${INPUT_COMMIT_AUTHOR_NAME}" \ + --author-email="${INPUT_COMMIT_AUTHOR_EMAIL}" \ + --working-dir="${INPUT_WORKING_DIR}" 2>&1 | tee log.txt) + +cat log.txt + +commit=$(echo "$output" | grep "Commit Hash:" | sed 's/.*: //') + +if [ -n "$commit" ]; then + echo "changes=true" >> "$GITHUB_OUTPUT" + echo "commit=$commit" >> "$GITHUB_OUTPUT" +else + echo "changes=false" >> "$GITHUB_OUTPUT" +fi diff --git a/tests/trestlebot/test_bot.py b/tests/trestlebot/test_bot.py index 233ff823..2d89550d 100644 --- a/tests/trestlebot/test_bot.py +++ b/tests/trestlebot/test_bot.py @@ -60,12 +60,13 @@ def test_local_commit(tmp_repo: Tuple[str, Repo]) -> None: repo.index.add(test_file_path) # Commit the test file - bot._local_commit( + commit_sha = bot._local_commit( repo, commit_user="Test User", commit_email="test@example.com", commit_message="Test commit message", ) + assert commit_sha != "" # Verify that the commit is made commit = next(repo.iter_commits()) @@ -91,13 +92,15 @@ def test_local_commit_with_committer(tmp_repo: Tuple[str, Repo]) -> None: repo.index.add(test_file_path) # Commit the test file - bot._local_commit( + commit_sha = bot._local_commit( repo, commit_user="Test Commit User", commit_email="test-committer@example.com", commit_message="Test commit message", ) + assert commit_sha != "" + # Verify that the commit is made commit = next(repo.iter_commits()) assert commit.message.strip() == "Test commit message" @@ -122,7 +125,7 @@ def test_local_commit_with_author(tmp_repo: Tuple[str, Repo]) -> None: repo.index.add(test_file_path) # Commit the test file - bot._local_commit( + commit_sha = bot._local_commit( repo, commit_user="Test User", commit_email="test@example.com", @@ -130,6 +133,7 @@ def test_local_commit_with_author(tmp_repo: Tuple[str, Repo]) -> None: author_name="The Author", author_email="author@test.com", ) + assert commit_sha != "" # Verify that the commit is made commit = next(repo.iter_commits()) @@ -153,7 +157,7 @@ def test_run_dry_run(tmp_repo: Tuple[str, Repo]) -> None: f.write("Test content") # Test running the bot - bot.run( + commit_sha = bot.run( working_dir=repo_path, branch="main", commit_name="Test User", @@ -164,6 +168,7 @@ def test_run_dry_run(tmp_repo: Tuple[str, Repo]) -> None: patterns=["*.txt"], dry_run=True, ) + assert commit_sha != "" # Verify that the commit is made commit = next(repo.iter_commits()) @@ -175,3 +180,24 @@ def test_run_dry_run(tmp_repo: Tuple[str, Repo]) -> None: assert os.path.basename(test_file_path) in commit.stats.files clean(repo_path, repo) + + +def test_empty_commit(tmp_repo: Tuple[str, Repo]) -> None: + """Test running bot with no file updates""" + repo_path, repo = tmp_repo + + # Test running the bot + commit_sha = bot.run( + working_dir=repo_path, + branch="main", + commit_name="Test User", + commit_email="test@example.com", + commit_message="Test commit message", + author_name="The Author", + author_email="author@test.com", + patterns=["*.txt"], + dry_run=True, + ) + assert commit_sha == "" + + clean(repo_path, repo) diff --git a/trestlebot/bot.py b/trestlebot/bot.py index 1bd109c1..269227bc 100644 --- a/trestlebot/bot.py +++ b/trestlebot/bot.py @@ -52,7 +52,7 @@ def _local_commit( commit_message: str, author_name: str = "", author_email: str = "", -) -> None: +) -> str: """Creates a local commit in git working directory""" try: # Set the user and email for the commit @@ -65,7 +65,10 @@ def _local_commit( author = Actor(name=author_name, email=author_email) # Commit the changes - gitwd.index.commit(commit_message, author=author) + commit = gitwd.index.commit(commit_message, author=author) + + # Return commit sha + return commit.hexsha except GitCommandError as e: raise RepoException(f"Git commit failed: {e}") from e @@ -81,8 +84,25 @@ def run( patterns: List[str], pre_tasks: Optional[List[TaskBase]] = None, dry_run: bool = False, -) -> int: - """Run Trestle Bot and return exit code""" +) -> str: + """Run Trestle Bot and return exit code + + Args: + working_dir: Location of the git repo + branch: Branch to put updates to + commit_name: Name of the user for commit creation + commit_email: Email of the user for commit creation + author_name: Name of the commit author + author_email: Email of the commit author + patterns: List of file patterns for `git add` + pre_tasks: Optional task list to executing before updating the workspace + dry_run: Only complete local work. Do not push. + + Returns: + A string containing the full commit sha. Defaults to "" if + there was no updates + """ + commit_sha: str = "" # Execute bot pre-tasks before committing repository updates if pre_tasks is not None: @@ -100,7 +120,7 @@ def run( _stage_files(repo, patterns) if repo.is_dirty(): - _local_commit( + commit_sha = _local_commit( repo, commit_name, commit_email, @@ -111,7 +131,7 @@ def run( if dry_run: logging.info("Dry run mode is enabled. Do not push to remote.") - return 0 + return commit_sha try: # Get the remote repository by name @@ -121,13 +141,13 @@ def run( remote.push(refspec=f"HEAD:{branch}") logging.info(f"Changes pushed to {branch} successfully.") - return 0 + return commit_sha except GitCommandError as e: raise RepoException(f"Git push to {branch} failed: {e}") from e else: logging.info("Nothing to commit") - return 0 + return commit_sha else: logging.info("Nothing to commit") - return 0 + return commit_sha diff --git a/trestlebot/cli.py b/trestlebot/cli.py index 716d16e6..649ddc9f 100644 --- a/trestlebot/cli.py +++ b/trestlebot/cli.py @@ -112,6 +112,15 @@ def _parse_cli_arguments() -> argparse.Namespace: return parser.parse_args() +def handle_exception( + exception: Exception, msg: str = "Exception occurred during execution" +) -> int: + """Log the exception and return the exit code""" + logging.exception(msg + f": {exception}") + + return 1 + + def run() -> None: """Trestle Bot entry point function.""" args = _parse_cli_arguments() @@ -145,16 +154,27 @@ def run() -> None: ) pre_tasks.append(assemble_task) - exit_code = bot.run( - working_dir=args.working_dir, - branch=args.branch, - commit_name=args.committer_name, - commit_email=args.committer_email, - commit_message=args.commit_message, - author_name=args.author_name, - author_email=args.author_email, - pre_tasks=pre_tasks, - patterns=args.patterns, - ) + exit_code: int = 0 + + # Assume it is a successful run, if the bot + # throws an exception update the exit code accordingly + try: + commit_sha = bot.run( + working_dir=args.working_dir, + branch=args.branch, + commit_name=args.committer_name, + commit_email=args.committer_email, + commit_message=args.commit_message, + author_name=args.author_name, + author_email=args.author_email, + pre_tasks=pre_tasks, + patterns=args.patterns, + ) + + # Print the full commit sha + print(f" Commit Hash: {commit_sha}") + + except Exception as e: + exit_code = handle_exception(e) sys.exit(exit_code)