Skip to content
Merged
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
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }} |

---

Expand Down
40 changes: 23 additions & 17 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,42 +17,48 @@ 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"
value: ${{ steps.check-changed-files.outputs.changed-files }}
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 }}"
fi
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
117 changes: 78 additions & 39 deletions src/check_changed_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down
31 changes: 22 additions & 9 deletions tests/test_check_changed_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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()
Expand Down