diff --git a/README.md b/README.md index d54e59e..1c91ce9 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,6 @@ This GitHub Action checks for changed files in a Git repository and validates th | checked_location | Enter the location of the files, separated by `;`. Example: `src/;docs/test.txt;tests/test*` | true | | | git_location | Path to the Git repository. | false | (current working directory) | | check_all_files | Enables the check of all defined files and folders in the directory. | false | false | -| github_user_token | Define the used GitHub server user token for Github.com. | false | ${{ github.token }} | -| action_version | Define the used version of the Github action. The parameter should only be used, if you want force overwrite the default version. | false | ${{ github.action_ref }} | --- diff --git a/action.yml b/action.yml index 2380db9..3651e50 100644 --- a/action.yml +++ b/action.yml @@ -17,14 +17,6 @@ inputs: description: "Enables the check of all defined files and folders in the directory" default: "false" required: false - github_user_token: - description: "Define the used GitHub server user token for Github.com" - default: ${{ github.token }} - required: false - action_version: - description: "Define the used version of the Github action. The parameter should only be used, if you want force overwrite the default version" - default: ${{ github.action_ref }} - required: false outputs: files_changed: description: "Returns true if changed files are detected and false by default" @@ -32,21 +24,35 @@ outputs: runs: using: "composite" steps: - - name: Checkout - uses: actions/checkout@v5 - with: - repository: ZPascal/check-changed-files-action - ref: ${{ inputs.action_version }} - github-server-url: https://github.com - token: ${{ inputs.github_user_token }} - - name: Install Python Dependencies run: python3 -m pip install pygit2 shell: bash - name: Check the changed files (wrapper) id: check-changed-files + env: + ACTION_REF: ${{ github.action_ref }} run: | + # Check if curl is installed, if not install it + if ! command -v curl &> /dev/null; then + echo "curl not found, installing..." + if [ -f /etc/alpine-release ]; then + # Alpine Linux + apk add --no-cache curl + elif [ -f /etc/debian_version ] || [ -f /etc/lsb-release ]; then + # Ubuntu/Debian + apt-get update && apt-get install -y curl + else + echo "Unsupported OS distribution" + exit 1 + fi + fi + + # Download the check_changed_files.py script + echo "Downloading check_changed_files.py script..." + echo $ACTION_REF + curl -s -L -o /tmp/check_changed_files.py https://raw.githubusercontent.com/ZPascal/check-changed-files-action/refs/heads/$ACTION_REF/src/check_changed_files.py + optional_flags="" if [ -n "${{ inputs.git_location }}" ]; then optional_flags+="-gl ${{ inputs.git_location }}" @@ -54,5 +60,5 @@ runs: if [ "${{ inputs.check_all_files }}" == "true" ]; then optional_flags+=" -caf" fi - echo "changed-files=$(python3 src/check_changed_files.py -cl '${{ inputs.checked_location }}' $optional_flags)" >> "$GITHUB_OUTPUT" + echo "changed-files='$(python3 /tmp/check_changed_files.py -cl '${{ inputs.checked_location }}' $optional_flags)'" >> "$GITHUB_OUTPUT" shell: bash diff --git a/src/check_changed_files.py b/src/check_changed_files.py index 6684985..3126e31 100644 --- a/src/check_changed_files.py +++ b/src/check_changed_files.py @@ -68,6 +68,20 @@ def _parse_args() -> argparse.Namespace: return arg_parser.parse_args() + @staticmethod + def _is_file_in_allowed_locations(file: str, checked_locations: list[str]) -> bool: + """ + Checks if a file path matches any of the allowed locations. + + Args: + file (str): The file path to check + checked_locations (list[str]): List of allowed location patterns + + Returns: + bool: True if the file matches any allowed location, False otherwise + """ + return any(loc in file for loc in checked_locations) + def _validate_git_path(self): """ Validates that the provided git location is a valid Git repository. @@ -113,50 +127,75 @@ def _get_changed_files(self) -> list[str]: changed_files.append(filepath) return changed_files + def _check_all_files_allowed(self, changed_files: list[str], checked_locations: list[str]) -> bool: + """ + Checks if ALL changed files are in allowed locations. + + Args: + changed_files (list[str]): List of changed file paths + checked_locations (list[str]): List of allowed location patterns + + Returns: + bool: True if all files are allowed, False otherwise + """ + if all(self._is_file_in_allowed_locations(file, checked_locations) for file in changed_files): + self._logger.info( + f"All changed files are allowed in checked locations {checked_locations}." + ) + return True + + for file in changed_files: + if not self._is_file_in_allowed_locations(file, checked_locations): + self._logger.info( + f"Changed file {file} is not a part of the checked locations {checked_locations}." + ) + break + return False + + def _check_any_file_allowed(self, changed_files: list[str], checked_locations: list[str]) -> bool: + """ + Checks if ANY changed file is in allowed locations. + + Args: + changed_files (list[str]): List of changed file paths + checked_locations (list[str]): List of allowed location patterns + + Returns: + bool: True if at least one file is allowed, False otherwise + """ + for file in changed_files: + if self._is_file_in_allowed_locations(file, checked_locations): + self._logger.info( + f"Changed file {file} is allowed in checked locations {checked_locations}." + ) + return True + + if changed_files: + self._logger.info( + f"Changed file {changed_files[0]} is not a part of the checked locations {checked_locations}." + ) + return False + def files_changed(self) -> bool: """ - Validates that all changed files are within the allowed checked locations. - """ - - changed_files: list[str] = self._get_changed_files() - checked_files_and_folders: list[str] = self._checked_location.split(";") - checked_files_and_folders_counter: int = 0 - - if len(changed_files) > 0: - for changed_file in changed_files: - for checked_files_and_folder in checked_files_and_folders: - if checked_files_and_folder in changed_file: - if self._check_all_files: - checked_files_and_folders_counter += 1 - - if len(changed_files) == checked_files_and_folders_counter: - self._logger.info( - f"All changed files are allowed in checked location {checked_files_and_folders}." - ) - return True - else: - self._logger.info( - f"Changed file {changed_file} is allowed in checked location " - f"{checked_files_and_folders}." - ) - return True - else: - file_found = False - for other_checked_location in checked_files_and_folders: - if other_checked_location in changed_file: - file_found = True - break - if not file_found and checked_files_and_folder == checked_files_and_folders[-1]: - self._logger.info( - f"Changed file {changed_file} is not a part of the checked location " - f"{checked_files_and_folders}." - ) - return False - return False - else: + Validates that changed files are within the allowed checked locations. + + Returns: + bool: True if validation passes based on check mode, False otherwise + """ + changed_files = self._get_changed_files() + checked_locations = self._checked_location.split(";") + + if not changed_files: self._logger.info("No changed files found.") return False + if self._check_all_files: + return self._check_all_files_allowed(changed_files, checked_locations) + else: + return self._check_any_file_allowed(changed_files, checked_locations) + + if __name__ == "__main__": checked_changed_files: CheckedChangedFiles = CheckedChangedFiles() files_changed: bool = checked_changed_files.files_changed() diff --git a/tests/test_check_changed_files.py b/tests/test_check_changed_files.py index 689863f..792b00a 100644 --- a/tests/test_check_changed_files.py +++ b/tests/test_check_changed_files.py @@ -135,6 +135,15 @@ def test_get_changed_files_key_error(self, mock_repo, mock_validate): f"Error: /repo is not a valid Git repository." ) + @patch("check_changed_files.CheckedChangedFiles._get_changed_files") + def test_files_changed_special_case(self, get_changed_files_mock): + get_changed_files_mock.return_value = ["releases/graphite-nozzle/graphite-nozzle-147.yml", + "releases/graphite-nozzle/index.yml"] + + self.check_changed_files._checked_location = "releases/graphite-nozzle/index.yml" + self.check_changed_files._logger = MagicMock() + self.assertEqual(True, self.check_changed_files.files_changed()) + @patch("check_changed_files.CheckedChangedFiles._get_changed_files") def test_files_changed_with_changes(self, get_changed_files_mock): get_changed_files_mock.return_value = ["src/test.py"] @@ -145,7 +154,7 @@ def test_files_changed_with_changes(self, get_changed_files_mock): self.assertEqual(True, self.check_changed_files.files_changed()) self.check_changed_files._logger.info.assert_called_with( - "Changed file src/test.py is allowed in checked location ['src/']." + "Changed file src/test.py is allowed in checked locations ['src/']." ) @patch("check_changed_files.CheckedChangedFiles._get_changed_files") @@ -168,7 +177,7 @@ def test_files_changed_with_changes_all_changes(self, get_changed_files_mock): self.assertEqual(True, self.check_changed_files.files_changed()) self.check_changed_files._logger.info.assert_called_with( - "All changed files are allowed in checked location ['src/']." + "All changed files are allowed in checked locations ['src/']." ) @patch("check_changed_files.CheckedChangedFiles._get_changed_files") @@ -182,7 +191,7 @@ def test_files_changed_multiple_locations(self, get_changed_files_mock): self.assertEqual(True, self.check_changed_files.files_changed()) self.check_changed_files._logger.info.assert_called_with( - "All changed files are allowed in checked location ['src', 'docs']." + "All changed files are allowed in checked locations ['src', 'docs']." ) @patch("check_changed_files.CheckedChangedFiles._get_changed_files") @@ -196,12 +205,12 @@ def test_files_changed_with_not_allowed_changes(self, get_changed_files_mock): self.assertEqual(False, self.check_changed_files.files_changed()) self.check_changed_files._logger.info.assert_called_with( - "Changed file temp/test.py is not a part of the checked location ['src/']." + "Changed file temp/test.py is not a part of the checked locations ['src/']." ) @patch("check_changed_files.CheckedChangedFiles._get_changed_files") def test_files_changed_partial_matches_all_files(self, get_changed_files_mock): - get_changed_files_mock.return_value = ["src/test.py", "temp/test.py"] + get_changed_files_mock.return_value = ["temp/test.py"] self.check_changed_files._checked_location = "src/" self.check_changed_files._check_all_files = True @@ -210,7 +219,7 @@ def test_files_changed_partial_matches_all_files(self, get_changed_files_mock): self.assertEqual(False, self.check_changed_files.files_changed()) self.check_changed_files._logger.info.assert_called_with( - "Changed file temp/test.py is not a part of the checked location ['src/']." + "Changed file temp/test.py is not a part of the checked locations ['src/']." ) @patch("check_changed_files.CheckedChangedFiles._get_changed_files") @@ -224,7 +233,7 @@ def test_files_changed_multiple_checks(self, get_changed_files_mock): self.assertEqual(True, self.check_changed_files.files_changed()) self.check_changed_files._logger.info.assert_called_with( - "Changed file src/subfolder/test.py is allowed in checked location ['docs/', 'src/', 'test/']." + "Changed file src/subfolder/test.py is allowed in checked locations ['docs/', 'src/', 'test/']." ) @patch("check_changed_files.CheckedChangedFiles._get_changed_files") @@ -241,9 +250,13 @@ def split(self, sep): self.assertFalse(self.check_changed_files.files_changed()) - @patch("os.path.exists", retun_value=True) + @patch("pygit2.Repository") @patch("os.path.isdir", return_value=True) - def test_main_prints_false(self, isdir_mock, exists_mock): + @patch("os.path.exists", return_value=True) + @patch("os.path.abspath", return_value="/test") + def test_main_prints_false(self, abspath_mock, exists_mock, isdir_mock, mock_repo): + mock_repo.return_value.status.return_value = {} + argv = ["check_changed_files.py", "-cl", "src", "-gl", "."] with patch.object(sys, "argv", argv): buf = io.StringIO()