diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 05e21065c..f6f1c6229 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -15,6 +15,6 @@ jobs: action: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v5.0.1 + - uses: dessant/lock-threads@v6.0.0 with: process-only: 'issues' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c48d32445..ff04716dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: - name: Python Semantic Release id: release - uses: python-semantic-release/python-semantic-release@v10.5.2 + uses: python-semantic-release/python-semantic-release@v10.5.3 with: github_token: ${{ secrets.RELEASE_GITHUB_TOKEN }} @@ -32,7 +32,7 @@ jobs: if: steps.release.outputs.released == 'true' - name: Publish package distributions to GitHub Releases - uses: python-semantic-release/publish-action@v10.5.2 + uses: python-semantic-release/publish-action@v10.5.3 if: steps.release.outputs.released == 'true' with: github_token: ${{ secrets.RELEASE_GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b10f50c21..0f2af115e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -77,7 +77,7 @@ jobs: TOXENV: ${{ matrix.toxenv }} run: tox -- --override-ini='log_cli=True' - name: Upload codecov coverage - uses: codecov/codecov-action@v5.5.1 + uses: codecov/codecov-action@v5.5.2 with: files: ./coverage.xml flags: ${{ matrix.toxenv }} @@ -100,7 +100,7 @@ jobs: TOXENV: cover run: tox - name: Upload codecov coverage - uses: codecov/codecov-action@v5.5.1 + uses: codecov/codecov-action@v5.5.2 with: files: ./coverage.xml flags: unit @@ -120,7 +120,7 @@ jobs: pip install -r requirements-test.txt - name: Build package run: python -m build -o dist/ - - uses: actions/upload-artifact@v5.0.0 + - uses: actions/upload-artifact@v6.0.0 with: name: dist path: dist @@ -134,7 +134,7 @@ jobs: uses: actions/setup-python@v6.1.0 with: python-version: '3.14' - - uses: actions/download-artifact@v6.0.0 + - uses: actions/download-artifact@v7.0.0 with: name: dist path: dist diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c9e61446b..3b93e8655 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: hooks: - id: black - repo: https://github.com/commitizen-tools/commitizen - rev: v4.10.0 + rev: v4.10.1 hooks: - id: commitizen stages: [commit-msg] @@ -32,7 +32,7 @@ repos: - requests-toolbelt==1.0.0 files: 'gitlab/' - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.0 + rev: v1.19.1 hooks: - id: mypy args: [] @@ -51,6 +51,6 @@ repos: - id: rst-directive-colons - id: rst-inline-touching-normal - repo: https://github.com/maxbrunet/pre-commit-renovate - rev: 42.39.4 + rev: 42.64.1 hooks: - id: renovate-config-validator diff --git a/gitlab/v4/objects/registry_protection_repository_rules.py b/gitlab/v4/objects/registry_protection_repository_rules.py index 19d4bdf59..2a457a024 100644 --- a/gitlab/v4/objects/registry_protection_repository_rules.py +++ b/gitlab/v4/objects/registry_protection_repository_rules.py @@ -1,5 +1,12 @@ from gitlab.base import RESTObject -from gitlab.mixins import CreateMixin, ListMixin, SaveMixin, UpdateMethod, UpdateMixin +from gitlab.mixins import ( + CreateMixin, + DeleteMixin, + ListMixin, + SaveMixin, + UpdateMethod, + UpdateMixin, +) from gitlab.types import RequiredOptional __all__ = [ @@ -16,6 +23,7 @@ class ProjectRegistryRepositoryProtectionRuleManager( ListMixin[ProjectRegistryRepositoryProtectionRule], CreateMixin[ProjectRegistryRepositoryProtectionRule], UpdateMixin[ProjectRegistryRepositoryProtectionRule], + DeleteMixin[ProjectRegistryRepositoryProtectionRule], ): _path = "/projects/{project_id}/registry/protection/repository/rules" _obj_cls = ProjectRegistryRepositoryProtectionRule diff --git a/requirements-docs.txt b/requirements-docs.txt index b83ef6dac..8d756d4f1 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ -r requirements.txt -furo==2025.9.25 +furo==2025.12.19 jinja2==3.1.6 myst-parser==4.0.1 sphinx==8.2.3 diff --git a/requirements-lint.txt b/requirements-lint.txt index 4eb6779f8..5006460d1 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -1,14 +1,14 @@ -r requirements.txt argcomplete==2.0.0 black==25.12.0 -commitizen==4.10.0 +commitizen==4.10.1 flake8==7.3.0 isort==7.0.0 -mypy==1.19.0 +mypy==1.19.1 pylint==4.0.4 pytest==9.0.2 responses==0.25.8 respx==0.22.0 types-PyYAML==6.0.12.20250915 types-requests==2.32.4.20250913 -types-setuptools==80.9.0.20250822 +types-setuptools==80.9.0.20251221 diff --git a/requirements-precommit.txt b/requirements-precommit.txt index 917f1957e..fc2379223 100644 --- a/requirements-precommit.txt +++ b/requirements-precommit.txt @@ -1 +1 @@ -pre-commit==4.5.0 +pre-commit==4.5.1 diff --git a/requirements-test.txt b/requirements-test.txt index e8843be94..b06434452 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,7 +1,7 @@ -r requirements.txt anyio==4.12.0 build==1.3.0 -coverage==7.12.0 +coverage==7.13.0 pytest-console-scripts==1.4.1 pytest-cov==7.0.0 pytest-github-actions-annotate-failures==0.3.0 diff --git a/tests/functional/api/test_epics.py b/tests/functional/api/test_epics.py index a4f6765da..b61e23776 100644 --- a/tests/functional/api/test_epics.py +++ b/tests/functional/api/test_epics.py @@ -15,18 +15,20 @@ def test_epics(group): assert group.epics.list() -@pytest.mark.xfail(reason="404 on issue.id") def test_epic_issues(epic, issue): assert not epic.issues.list() + # FYI: Creating an issue causes a note to be created epic_issue = epic.issues.create({"issue_id": issue.id}) assert epic.issues.list() + # FYI: Deleting an issue causes a note to be created epic_issue.delete() def test_epic_notes(epic): - assert not epic.notes.list() + notes = epic.notes.list(get_all=True) epic.notes.create({"body": "Test note"}) - assert epic.notes.list() + new_notes = epic.notes.list(get_all=True) + assert len(new_notes) == (len(notes) + 1), f"{new_notes} {notes}" diff --git a/tests/functional/api/test_keys.py b/tests/functional/api/test_keys.py index 359649bef..6a2d660ed 100644 --- a/tests/functional/api/test_keys.py +++ b/tests/functional/api/test_keys.py @@ -38,6 +38,14 @@ def test_keys_deploy(gl, project, DEPLOY_KEY): key_by_fingerprint = gl.keys.get(fingerprint=fingerprint) assert key_by_fingerprint.title == key.title assert key_by_fingerprint.key == key.key - assert len(key_by_fingerprint.deploy_keys_projects) == 1 + + if not any( + key_project.get("project_id") == project.id + for key_project in key_by_fingerprint.deploy_keys_projects + ): + raise AssertionError( + f"Project {project} not found in 'deploy_keys_projects' " + f"{key_by_fingerprint.pformat()}" + ) key.delete() diff --git a/tests/functional/api/test_project_job_token_scope.py b/tests/functional/api/test_project_job_token_scope.py index 0d0466182..b1de0a7b2 100644 --- a/tests/functional/api/test_project_job_token_scope.py +++ b/tests/functional/api/test_project_job_token_scope.py @@ -1,3 +1,6 @@ +import pytest + + # https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html#allow-any-project-to-access-your-project def test_enable_limit_access_to_this_project(gl, project): scope = project.job_token_scope.get() @@ -10,6 +13,7 @@ def test_enable_limit_access_to_this_project(gl, project): assert scope.inbound_enabled +@pytest.mark.xfail(reason="https://gitlab.com/gitlab-org/gitlab/-/issues/582271") def test_disable_limit_access_to_this_project(gl, project): scope = project.job_token_scope.get() diff --git a/tests/functional/api/test_projects.py b/tests/functional/api/test_projects.py index 760f95336..c56b23ec7 100644 --- a/tests/functional/api/test_projects.py +++ b/tests/functional/api/test_projects.py @@ -26,9 +26,9 @@ def test_create_project(gl, user): sudo_project = gl.projects.create({"name": "sudo_project"}, sudo=user.id) - created = gl.projects.list() + created = gl.projects.list(get_all=True) created_gen = gl.projects.list(iterator=True) - owned = gl.projects.list(owned=True) + owned = gl.projects.list(owned=True, get_all=True) assert admin_project in created and sudo_project in created assert admin_project in owned and sudo_project not in owned diff --git a/tests/functional/api/test_registry.py b/tests/functional/api/test_registry.py index 91fdceacc..d234128ca 100644 --- a/tests/functional/api/test_registry.py +++ b/tests/functional/api/test_registry.py @@ -26,3 +26,8 @@ def test_project_protected_registry(project: Project): protected_registry.minimum_access_level_for_push = "owner" protected_registry.save() assert protected_registry.minimum_access_level_for_push == "owner" + + protected_registry.delete() + + rules = project.registry_protection_repository_rules.list() + assert rules == [] diff --git a/tests/functional/fixtures/.env b/tests/functional/fixtures/.env index e85f85e6f..f33c35752 100644 --- a/tests/functional/fixtures/.env +++ b/tests/functional/fixtures/.env @@ -1,4 +1,4 @@ GITLAB_IMAGE=gitlab/gitlab-ee -GITLAB_TAG=17.8.2-ee.0 +GITLAB_TAG=18.7.0-ee.0 GITLAB_RUNNER_IMAGE=gitlab/gitlab-runner GITLAB_RUNNER_TAG=96856197 diff --git a/tests/functional/fixtures/docker-compose.yml b/tests/functional/fixtures/docker-compose.yml index f36f3d2fd..17562d5be 100644 --- a/tests/functional/fixtures/docker-compose.yml +++ b/tests/functional/fixtures/docker-compose.yml @@ -34,7 +34,7 @@ services: entrypoint: - /bin/sh - -c - - ruby /create_license.rb && /assets/wrapper + - ruby /create_license.rb && /assets/init-container volumes: - ${PWD}/tests/functional/fixtures/create_license.rb:/create_license.rb ports: diff --git a/tests/functional/helpers.py b/tests/functional/helpers.py index 090673bf7..9d313e540 100644 --- a/tests/functional/helpers.py +++ b/tests/functional/helpers.py @@ -9,6 +9,7 @@ import gitlab import gitlab.base import gitlab.exceptions +import gitlab.v4.objects SLEEP_INTERVAL = 0.5 TIMEOUT = 60 # seconds before timeout will occur @@ -37,6 +38,11 @@ def safe_delete(object: gitlab.base.RESTObject) -> None: object = manager.get(object.get_id()) # type: ignore[attr-defined] except gitlab.exceptions.GitlabGetError: return + # If object is already marked for deletion we have succeeded + if getattr(object, "marked_for_deletion_on", None) is not None: + # 'Group' and 'Project' objects have a 'marked_for_deletion_on' attribute + logging.info(f"{object!r} is marked for deletion.") + return if index: logging.info(f"Attempt {index + 1} to delete {object!r}.") @@ -52,22 +58,16 @@ def safe_delete(object: gitlab.base.RESTObject) -> None: # we shouldn't cause test to fail if it still exists return elif isinstance(object, gitlab.v4.objects.Project): - # Immediately delete rather than waiting for at least 1day - # https://docs.gitlab.com/ee/api/projects.html#delete-project - object.delete(permanently_remove=True) - pass + # Starting in GitLab 18, projects can't be immediately deleted. + # So this will mark it for deletion. + object.delete() else: # We only attempt to delete parent groups to prevent dangling sub-groups - # However parent groups can only be deleted on a delay in Gl 16 + # However parent groups can only be deleted on a delay in GitLab 16 # https://docs.gitlab.com/ee/api/groups.html#remove-group object.delete() except gitlab.exceptions.GitlabDeleteError: - logging.info(f"{object!r} already deleted or scheduled for deletion.") - if isinstance(object, gitlab.v4.objects.Group): - # Parent groups can never be immediately deleted in GL 16, - # so don't cause test to fail if it still exists - return - pass + logging.exception(f"Error attempting to delete: {object.pformat()}") time.sleep(SLEEP_INTERVAL) pytest.fail(f"{object!r} was not deleted") diff --git a/tests/unit/objects/test_registry_protection_rules.py b/tests/unit/objects/test_registry_protection_rules.py index 3078278f5..3e9db414a 100644 --- a/tests/unit/objects/test_registry_protection_rules.py +++ b/tests/unit/objects/test_registry_protection_rules.py @@ -58,6 +58,17 @@ def resp_update_protected_registry(): yield rsps +@pytest.fixture +def resp_delete_protected_registry(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.DELETE, + url="http://localhost/api/v4/projects/1/registry/protection/repository/rules/1", + status=204, + ) + yield rsps + + def test_list_project_protected_registries(project, resp_list_protected_registries): protected_registry = project.registry_protection_repository_rules.list()[0] assert isinstance(protected_registry, ProjectRegistryRepositoryProtectionRule) @@ -80,3 +91,7 @@ def test_update_project_protected_registry(project, resp_update_protected_regist 1, {"repository_path_pattern": "abc*"} ) assert updated["repository_path_pattern"] == "abc*" + + +def test_delete_project_protected_registry(project, resp_delete_protected_registry): + project.registry_protection_repository_rules.delete(1)