diff --git a/README.md b/README.md index 8580da3..0820e23 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,13 @@ Supports Unix-shell style wildcards, i.e 'sha-*' to match all tags starting with Whether to consider untagged images for deletion. +## dry-run + +* **Required**: `No` +* **Default**: `false` + +Prints output showing imaages which would be deleted but does not actually delete any images. + # Outputs ## deleted diff --git a/action.yml b/action.yml index a23d4c9..6bc80b5 100644 --- a/action.yml +++ b/action.yml @@ -42,6 +42,10 @@ inputs: description: "Whether to consider untagged images for deletion." required: false default: 'true' + dry-run: + description: "Do not actually delete images. Print output showing what would have been deleted." + required: false + default: 'false' outputs: needs-github-assistance: description: 'Comma-separated list of image names and tags, for image versions that are public and have more than 5000 downloads.' @@ -84,7 +88,8 @@ runs: "$SKIP_TAGS" \ "$KEEP_AT_LEAST" \ "$FILTER_TAGS" \ - "$FILTER_INCLUDE_UNTAGGED" + "$FILTER_INCLUDE_UNTAGGED" \ + "$DRY_RUN" env: ACCOUNT_TYPE: "${{ inputs.account-type }}" ORG_NAME: "${{ inputs.org-name }}" @@ -97,3 +102,4 @@ runs: KEEP_AT_LEAST: "${{ inputs.keep-at-least }}" FILTER_TAGS: "${{ inputs.filter-tags }}" FILTER_INCLUDE_UNTAGGED: "${{ inputs.filter-include-untagged }}" + DRY_RUN: "${{ inputs.dry-run }}" diff --git a/main.py b/main.py index d2d4359..e83db53 100644 --- a/main.py +++ b/main.py @@ -298,6 +298,7 @@ class Inputs(BaseModel): keep_at_least: conint(ge=0) = 0 # type: ignore[valid-type] filter_tags: list[str] filter_include_untagged: bool = True + dry_run: bool = False @validator('skip_tags', 'filter_tags', 'image_names', pre=True) def parse_comma_separate_string_as_list(cls, v: str) -> list[str]: @@ -385,6 +386,10 @@ async def get_and_delete_old_versions(image_name: str, inputs: Inputs, http_clie # Skipping because this image version is tagged with a protected tag delete_image = False + if inputs.dry_run: + delete_image = False + print(f'Would delete image {image_name}:{version.id}.') + if delete_image: tasks.append( asyncio.create_task( @@ -460,6 +465,7 @@ async def main( keep_at_least: str, filter_tags: str, filter_include_untagged: str, + dry_run: str = 'false', ) -> None: """ Delete old image versions. @@ -486,6 +492,8 @@ async def main( :param filter_tags: Comma-separated list of tags to consider for deletion. Supports wildcard '*', '?', '[seq]' and '[!seq]' via Unix shell-style wildcards :param filter_include_untagged: Whether to consider untagged images for deletion. + :param dry_run: Do not actually delete packages but print output showing which packages would + have been deleted. """ inputs = Inputs( image_names=image_names, @@ -498,6 +506,7 @@ async def main( keep_at_least=keep_at_least, filter_tags=filter_tags, filter_include_untagged=filter_include_untagged, + dry_run=dry_run, ) async with AsyncClient( headers={'accept': 'application/vnd.github.v3+json', 'Authorization': f'Bearer {token}'} diff --git a/main_tests.py b/main_tests.py index da364df..b461f3a 100644 --- a/main_tests.py +++ b/main_tests.py @@ -155,6 +155,7 @@ def test_post_deletion_output(capsys, ok_response, bad_response): 'filter_include_untagged': 'true', 'token': 'test', 'account_type': 'personal', + 'dry_run': 'false', } @@ -344,6 +345,19 @@ async def test_filter_tags(self, mocker, capsys, http_client): captured = capsys.readouterr() assert captured.out == 'Deleted old image: a:1234567\n' + async def test_dry_run(self, mocker, capsys, http_client): + data = deepcopy(self.valid_data) + data[0].metadata = MetadataModel( + **{'container': {'tags': ['sha-deadbeef', 'edge']}, 'package_type': 'container'} + ) + mocker.patch.object(main.GithubAPI, 'list_package_versions', return_value=data) + mock_delete_package = mocker.patch.object(main.GithubAPI, 'delete_package') + inputs = _create_inputs_model(dry_run='true') + await get_and_delete_old_versions(image_name='a', inputs=inputs, http_client=http_client) + captured = capsys.readouterr() + assert captured.out == 'Would delete image a:1234567.\nNo more versions to delete for a\n' + mock_delete_package.assert_not_called() + def test_inputs_bad_account_type(): # Account type