diff --git a/.github/workflows/experimental-check-version-pinning.yml b/.github/workflows/experimental-check-version-pinning.yml index 99c3f78d8..26a3a4160 100644 --- a/.github/workflows/experimental-check-version-pinning.yml +++ b/.github/workflows/experimental-check-version-pinning.yml @@ -5,6 +5,7 @@ on: branches: - main pull_request: + workflow_dispatch: jobs: check-version-pinning: @@ -15,19 +16,8 @@ jobs: with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Set up Python - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 + - name: Check for unpinned Actions + uses: ministryofjustice/github-actions/check-version-pinning@ccf9e3a4a828df1ec741f6c8e6ed9d0acaef3490 with: - python-version: "3.11" - cache: "pipenv" - - - name: Install Pipenv - run: pip install pipenv - - - name: Verify Pipfile.lock is in sync - run: pipenv verify - - name: Install dependencies - run: pipenv install - - - name: Check for incorrectly pinned Actions - run: pipenv run python3 -m bin.check_version_pinning + workflow_directory: ".github/workflows" + scan_mode: "full" diff --git a/bin/check_version_pinning.py b/bin/check_version_pinning.py deleted file mode 100644 index 830019689..000000000 --- a/bin/check_version_pinning.py +++ /dev/null @@ -1,63 +0,0 @@ -import os -import sys - -import yaml - - -def find_workflow_files(workflow_directory=".github/workflows"): - for root, _, files in os.walk(workflow_directory): - for file in files: - if file.endswith(".yml") or file.endswith(".yaml"): - yield os.path.join(root, file) - - -def parse_yaml_file(file_path): - with open(file_path, "r", buffering=-1, encoding="utf-8") as f: - try: - return yaml.safe_load(f) - except yaml.YAMLError as e: - print(f"Error parsing {file_path}: {e}") - return None - - -def check_uses_field_in_workflow(workflows, file_path): - results = [] - if workflows: - for job in workflows.get("jobs", {}).values(): - for step in job.get("steps", []): - uses = step.get("uses", "") - if "@v" in uses and not ( - "actions/" in uses or "ministryofjustice" in uses - ): - results.append(f"{file_path}: {uses}") - return results - - -def check_version_pinning(workflow_directory=".github/workflows"): - all_results = [] - - for file_path in find_workflow_files(workflow_directory): - workflows = parse_yaml_file(file_path) - if workflows: - results = check_uses_field_in_workflow(workflows, file_path) - all_results.extend(results) - - if all_results: - print( - "It may not be related to this PR, but the following third-party\n" - "GitHub Actions are using version pinning rather than SHA hash pinning:\n" - ) - for result in all_results: - print(f" - {result}") - - print( - "\nPlease see the following documentation for more information:\n" - "https://tinyurl.com/3sev9etr" - ) - sys.exit(1) - else: - print("No workflows found with pinned versions (@v).") - - -if __name__ == "__main__": - check_version_pinning() diff --git a/test/test_bin/test_check_version_pinning.py b/test/test_bin/test_check_version_pinning.py deleted file mode 100644 index eed4a16d7..000000000 --- a/test/test_bin/test_check_version_pinning.py +++ /dev/null @@ -1,118 +0,0 @@ -import unittest -from unittest.mock import mock_open, patch - -from bin.check_version_pinning import check_version_pinning - - -class TestCheckVersionPinning(unittest.TestCase): - - @patch("os.walk") - @patch("builtins.open", new_callable=mock_open) - @patch("yaml.safe_load") - def test_no_yaml_files(self, mock_yaml_load, mock_open_file, mock_os_walk): - # Simulate os.walk returning no .yml or .yaml files - _ = mock_open_file - mock_os_walk.return_value = [(".github/workflows", [], [])] - mock_yaml_load.return_value = None - - with patch("builtins.print") as mock_print: - check_version_pinning() - mock_print.assert_called_once_with( - "No workflows found with pinned versions (@v)." - ) - - @patch("os.walk") - @patch("builtins.open", new_callable=mock_open) - @patch("yaml.safe_load") - def test_yaml_file_without_uses(self, mock_yaml_load, mock_open_file, mock_os_walk): - _ = mock_open_file - mock_os_walk.return_value = [(".github/workflows", [], ["workflow.yml"])] - mock_yaml_load.return_value = { - "jobs": {"build": {"steps": [{"name": "Checkout code"}]}} - } - - with patch("builtins.print") as mock_print: - check_version_pinning() - mock_print.assert_called_once_with( - "No workflows found with pinned versions (@v)." - ) - - @patch("os.walk") - @patch("builtins.open", new_callable=mock_open) - @patch("yaml.safe_load") - def test_workflow_with_pinned_version( - self, mock_yaml_load, mock_open_file, mock_os_walk - ): - # Simulate a workflow file with a pinned version (@v) - _ = mock_open_file - mock_os_walk.return_value = [(".github/workflows", [], ["workflow.yml"])] - mock_yaml_load.return_value = { - "jobs": {"build": {"steps": [{"uses": "some-org/some-action@v1.0.0"}]}} - } - - with patch("builtins.print") as mock_print, self.assertRaises(SystemExit) as cm: - check_version_pinning() - mock_print.assert_any_call("Found workflows with pinned versions (@v):") - mock_print.assert_any_call( - ".github/workflows/workflow.yml: some-org/some-action@v1.0.0" - ) - self.assertEqual(cm.exception.code, 1) - - @patch("os.walk") - @patch("builtins.open", new_callable=mock_open) - @patch("yaml.safe_load") - def test_workflow_ignoring_actions( - self, mock_yaml_load, mock_open_file, mock_os_walk - ): - _ = mock_open_file - # Simulate a workflow file with an action to be ignored - mock_os_walk.return_value = [(".github/workflows", [], ["workflow.yml"])] - mock_yaml_load.return_value = { - "jobs": { - "build": { - "steps": [ - {"uses": "actions/setup-python@v2"}, - {"uses": "ministryofjustice/some-action@v1.0.0"}, - ] - } - } - } - - with patch("builtins.print") as mock_print: - check_version_pinning() - mock_print.assert_called_once_with( - "No workflows found with pinned versions (@v)." - ) - - @patch("os.walk") - @patch("builtins.open", new_callable=mock_open) - @patch("yaml.safe_load") - def test_workflow_with_mixed_versions( - self, mock_yaml_load, mock_open_file, mock_os_walk - ): - _ = mock_open_file - # Simulate a workflow with both ignored and non-ignored actions - mock_os_walk.return_value = [(".github/workflows", [], ["workflow.yml"])] - mock_yaml_load.return_value = { - "jobs": { - "build": { - "steps": [ - {"uses": "actions/setup-python@v2"}, - {"uses": "some-org/some-action@v1.0.0"}, - {"uses": "ministryofjustice/some-action@v1.0.0"}, - ] - } - } - } - - with patch("builtins.print") as mock_print, self.assertRaises(SystemExit) as cm: - check_version_pinning() - mock_print.assert_any_call("Found workflows with pinned versions (@v):") - mock_print.assert_any_call( - ".github/workflows/workflow.yml: some-org/some-action@v1.0.0" - ) - self.assertEqual(cm.exception.code, 1) - - -if __name__ == "__main__": - unittest.main()