Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
73c69bc
refactor(skills): remove the GitLab die() helper in favor of GitLabError
Aug 25, 2026
744ec1c
Merge branch 'main' into refactor/1555-gitlab-single-failure-mechanism
jkim323 Aug 25, 2026
9a63117
fix(skills): resolve GitLab lint and test failures
Aug 25, 2026
230401a
test(skills): tighten GitLab main guard contract
Aug 26, 2026
442dcea
fix(skills): redact GitLab OAuth traceback causes
Aug 26, 2026
862942f
fix(scripts): self-register PSGallery for module installs
Aug 26, 2026
4139c73
test(skills): enforce GitLab branch coverage
Aug 26, 2026
69af117
fix(scripts): satisfy PowerShell and table lint
Aug 27, 2026
3f6de01
Merge branch 'main' into refactor/1555-gitlab-single-failure-mechanism
jkim323 Aug 27, 2026
9150c49
Merge branch 'main' into refactor/1555-gitlab-single-failure-mechanism
jkim323 Aug 27, 2026
448f583
Merge branch 'main' into refactor/1555-gitlab-single-failure-mechanism
jkim323 Aug 30, 2026
50ec901
Merge branch 'main' into refactor/1555-gitlab-single-failure-mechanism
jkim323 Aug 31, 2026
a2514e5
fix(scripts): use default PSGallery registration
jkim323 Aug 31, 2026
b915975
Merge branch 'main' into refactor/1555-gitlab-single-failure-mechanism
WilliamBerryiii Sep 1, 2026
4770a11
fix(skills): type GitLab response validation failures
jkim323 Sep 1, 2026
5ea8121
Merge branch 'main' into refactor/1555-gitlab-single-failure-mechanism
jkim323 Sep 3, 2026
98bc8a0
fix(build): remediate npm audit vulnerabilities
Copilot Sep 3, 2026
3249071
test(skills): detect aliased GitLab exit mechanisms
jkim323 Sep 3, 2026
e8869aa
Merge branch 'refactor/1555-gitlab-single-failure-mechanism' of https…
jkim323 Sep 3, 2026
9e74a3c
fix(scripts): recover from PSGallery registration no-op
jkim323 Sep 3, 2026
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
5 changes: 5 additions & 0 deletions .github/skills/project-planning/gitlab/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ fuzz = [
testpaths = ["tests"]
pythonpath = ["scripts"]
python_files = ["test_*.py", "fuzz_harness.py"]
addopts = "--cov --cov-report=term-missing --cov-fail-under=80"

[tool.coverage.run]
branch = true
source = ["scripts"]

[tool.ruff]
line-length = 88
Expand Down
213 changes: 122 additions & 91 deletions .github/skills/project-planning/gitlab/scripts/gitlab.py

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions .github/skills/project-planning/gitlab/tests/fuzz_harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@
FUZZING = True


# A CLI failure surfaces as SystemExit from the dispatch-layer die() helper
# or as GitLabError from a library-level helper that promises a return
# value. Both are expected refusals, not fuzz findings.
_EXPECTED_CLI_ERRORS = (SystemExit, gitlab.GitLabError)
# A CLI failure surfaces as GitLabError, the module's single failure
# mechanism. Nothing raises SystemExit outside the __main__ guard, so an
# expected refusal is always this one type, not a fuzz finding.
_EXPECTED_CLI_ERRORS = (gitlab.GitLabError,)


def fuzz_strip_git_suffix(data: bytes) -> None:
Expand Down Expand Up @@ -200,7 +200,7 @@ def test_validate_numeric_id_accepts_digits(self, value: str) -> None:

@pytest.mark.parametrize("value", ["", "abc", "12a", "-1"])
def test_validate_numeric_id_rejects_invalid_values(self, value: str) -> None:
with pytest.raises(SystemExit):
with pytest.raises(gitlab.GitLabError):
gitlab.validate_numeric_id(value)

def test_extract_field_handles_nested_values(self) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def test_audit_fail_closed_blocks_request(
_enable_audit(monkeypatch, log)
opener = mocker.patch("gitlab._OPENER.open")

with pytest.raises(SystemExit):
with pytest.raises(gitlab.GitLabError):
gitlab.request("GET", f"{TEST_API_URL}/projects/1/merge_requests/2")

opener.assert_not_called()
Expand Down Expand Up @@ -214,7 +214,7 @@ def test_oauth_attempt_failure_blocks_egress(
monkeypatch.setenv("GITLAB_AUDIT_LOG", str(tmp_path / "missing" / "audit.jsonl"))
opener = mocker.MagicMock()

with pytest.raises(SystemExit):
with pytest.raises(gitlab.GitLabError):
gitlab.oauth.post_form(
"https://gitlab.example.com",
"/oauth/token",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,12 @@ def _assert_usage_error(
command: CommandFn,
args: list[str],
expected_message: str,
capsys: pytest.CaptureFixture[str],
) -> None:
with pytest.raises(SystemExit) as exc_info:
with pytest.raises(gitlab.GitLabError) as exc_info:
command(args)

assert exc_info.value.code == gitlab.EXIT_USAGE
assert expected_message in capsys.readouterr().err
assert exc_info.value.exit_code == gitlab.EXIT_USAGE
assert expected_message in str(exc_info.value)


@pytest.mark.parametrize(
Expand All @@ -103,9 +102,8 @@ def test_commands_require_minimum_arguments(
command: CommandFn,
args: list[str],
expected_message: str,
capsys: pytest.CaptureFixture[str],
) -> None:
_assert_usage_error(command, args, expected_message, capsys)
_assert_usage_error(command, args, expected_message)


@pytest.mark.parametrize(
Expand Down Expand Up @@ -289,11 +287,10 @@ def test_write_commands_require_stdin_or_inline_content(
command: CommandFn,
args: list[str],
usage_message: str,
capsys: pytest.CaptureFixture[str],
) -> None:
stdin_factory("")

_assert_usage_error(command, args, usage_message, capsys)
_assert_usage_error(command, args, usage_message)


def test_mr_notes_uses_default_max_results(
Expand Down Expand Up @@ -349,9 +346,9 @@ def test_redacts_and_truncates_job_log_output(
assert "abc123" not in output
assert "... [truncated]" in output

def test_requires_job_id(self, capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(SystemExit) as exc_info:
def test_requires_job_id(self) -> None:
with pytest.raises(gitlab.GitLabError) as exc_info:
gitlab.cmd_job_log([])

assert exc_info.value.code == gitlab.EXIT_USAGE
assert USAGE_JOB_LOG in capsys.readouterr().err
assert exc_info.value.exit_code == gitlab.EXIT_USAGE
assert USAGE_JOB_LOG in str(exc_info.value)
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,24 @@ def test_is_loopback_rejects_empty_host() -> None:


def test_validate_project_path_rejects_traversal() -> None:
with pytest.raises(SystemExit):
with pytest.raises(gitlab.GitLabError):
gitlab._validate_project_path("../escape")


def test_validate_project_path_rejects_empty() -> None:
with pytest.raises(SystemExit):
with pytest.raises(gitlab.GitLabError):
gitlab._validate_project_path("")


def test_validate_numeric_id_rejects_non_numeric() -> None:
with pytest.raises(SystemExit):
with pytest.raises(gitlab.GitLabError):
gitlab.validate_numeric_id("abc")


def test_validate_numeric_id_rejects_out_of_range() -> None:
with pytest.raises(SystemExit):
with pytest.raises(gitlab.GitLabError):
gitlab.validate_numeric_id("0")
with pytest.raises(SystemExit):
with pytest.raises(gitlab.GitLabError):
gitlab.validate_numeric_id(str(gitlab.MAX_NUMERIC_ID + 1))


Expand All @@ -70,7 +70,7 @@ class _Response:
def read(self, _amount: int) -> bytes:
return b"x" * 32

with pytest.raises(SystemExit):
with pytest.raises(gitlab.GitLabError):
gitlab._read_capped(_Response(), 16, fail_on_limit=True)


Expand All @@ -96,7 +96,7 @@ def read(self, _amount: int | None = None) -> bytes:

mocker.patch("gitlab._OPENER.open", return_value=_Response())

with pytest.raises(SystemExit):
with pytest.raises(gitlab.GitLabError):
gitlab._request_bytes("GET", f"{TEST_API_URL}/projects/1", require_json=True)


Expand Down Expand Up @@ -131,7 +131,7 @@ def test_mr_update_rejects_oversized_stdin(
monkeypatch.setenv("GITLAB_PROJECT", "group/project")
stdin_factory("x" * (gitlab.MAX_BODY_BYTES + 1)) # type: ignore[operator]

with pytest.raises(SystemExit):
with pytest.raises(gitlab.GitLabError):
gitlab.cmd_mr_update(["9"])


Expand All @@ -143,5 +143,5 @@ def test_mr_comment_rejects_oversized_stdin(
monkeypatch.setenv("GITLAB_PROJECT", "group/project")
stdin_factory("x" * (gitlab.MAX_BODY_BYTES + 1)) # type: ignore[operator]

with pytest.raises(SystemExit):
with pytest.raises(gitlab.GitLabError):
gitlab.cmd_mr_comment(["9"])
Loading
Loading