Skip to content

Commit 0c4a8d4

Browse files
authored
Add Gitlab artifacts support (#36)
- Add Gitlab artifact list and grep commands - Include Grep options on SKILL.md
1 parent e29954b commit 0c4a8d4

11 files changed

Lines changed: 1018 additions & 6 deletions

File tree

skills/smith/SKILL.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@ Full vocabulary and flags live in `references/usage-recipes.md`. The minimum you
4040
| Discovery | `smith <azdo-remote-name> orgs`, `smith <github-remote-name> orgs`, `smith <gitlab-remote-name> groups`, `smith <azdo-remote-name> repos <project>`, `smith <github-remote-name> repos`, `smith <gitlab-remote-name> repos` |
4141
| Focused grep | `smith <azdo-remote-name> code grep <project> <repo> "<regex>"`, `smith <github-remote-name> code grep <repo> "<regex>"`, `smith <gitlab-remote-name> code grep <group/project> "<regex>"` |
4242
| PRs / MRs | `smith <azdo-remote-name> prs search`, `smith <github-remote-name> prs search`, `smith <gitlab-remote-name> prs search`, `smith <github-remote-name> prs list <repo>`, `smith <gitlab-remote-name> prs list <group/project>` |
43-
| Pipelines | `smith <github-remote-name> pipelines list <repo> <id>`, `smith <gitlab-remote-name> pipelines list <group/project> <id>`, `smith <github-remote-name> pipelines grep <repo> <id> "<regex>"`, `smith <gitlab-remote-name> pipelines grep <group/project> <id> "<regex>"` |
43+
| Pipelines | `smith <github-remote-name> pipelines list <repo> <id>`, `smith <gitlab-remote-name> pipelines list <group/project> <id>`, `smith <github-remote-name> pipelines grep <repo> <id> "<regex>"`, `smith <gitlab-remote-name> pipelines grep <group/project> <id> "<regex>"`, `smith <gitlab-remote-name> pipelines artifacts list <group/project> <pipeline-id> <job-id>`, `smith <gitlab-remote-name> pipelines artifacts grep <group/project> <pipeline-id> <job-id> "<regex>"` |
4444
| Stories / Issues | `smith <azdo-remote-name> stories search <project> --query`, `smith <gitlab-remote-name> stories search <group/project> --query`, `smith <youtrack-remote-name> stories search --query` |
4545

46+
All grep commands (code, pipeline logs, artifacts) support: `--path`, `--glob`, `--output-mode` (content/files_with_matches/count), `--context-lines`, `--from-line`/`--to-line`, `--reverse`, `--case-sensitive`. Code grep adds: `--branch`, `--no-clone`. Pipeline grep adds: `--log-id`.
47+
4648
Rules that save retries:
4749

4850
- **GitHub**: repo arg is bare `<repo>`, not `org/repo`. Search output may look like `org/repo:path` but commands still take `<repo>`.
@@ -51,6 +53,7 @@ Rules that save retries:
5153
- **YouTrack**: no repo arg; only issue IDs (e.g. `RAD-1055`) and `--query`.
5254
- Global `smith code search` and `smith prs search` target every enabled remote and reject `--project` or `--repo`. Use `smith <remote> ...` to narrow.
5355
- `pipelines grep ... <id>` expects a pipeline/run/build ID. For a specific job or log, call `pipelines list ...` first to find the parent ID, then `pipelines grep ... <pipeline-id> ".*" --log-id <job-or-log-id>`.
56+
- `pipelines artifacts ... <pipeline-id> <job-id>` is GitLab-only. Use `artifacts list` to enumerate archive paths and `artifacts grep`
5457
- `pipelines list ... <id>` prints a compact DAG (`@` pipelines, `#` stages, `*` jobs, inline `<needs` and `>>` downstream). GitLab traverses child pipelines via GraphQL (REST fallback emits header-only rows with a warning). Filter with `--status`, `--grep`, `--skip`/`--take`, `--max-depth` (gitlab only, default 0 = unlimited). Full grammar lives in `references/pipelines-format.md`.
5558

5659
Use `--help` on any command for flags.
@@ -67,6 +70,7 @@ Use `--help` on any command for flags.
6770
### Pipeline Analysis
6871
1. Use `smith pipelines list <repo> <pipeline_id> --status failed` to focus on failed jobs.
6972
2. Once you know the pipeline log ID, use `smith pipelines grep <repo> <pipeline_id> <log_id> --reverse` to analyze the logs.
73+
3. For GitLab artifact-backed failures, use `smith <gitlab-remote-name> pipelines artifacts list <group/project> <pipeline_id> <job_id>` and then `... artifacts grep ... "<regex>"`.
7074
## Stop Conditions
7175

7276
Stop narrowing and answer when any of these is true:

src/smith/cli/handlers.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,47 @@ def handle_ci_grep(client: SmithClient, args: argparse.Namespace) -> int:
633633
)
634634

635635

636+
def handle_ci_artifacts_list(client: SmithClient, args: argparse.Namespace) -> int:
637+
data = client.execute_ci_artifacts_list(
638+
remote_or_provider=_selected_target(args),
639+
project=getattr(args, "project", None),
640+
repo=getattr(args, "repo", None),
641+
pipeline_id=args.id,
642+
job_id=args.job_id,
643+
)
644+
return _emit_success(
645+
args=args,
646+
command=args.command_id,
647+
data=data,
648+
partial=_is_partial_result(data),
649+
)
650+
651+
652+
def handle_ci_artifacts_grep(client: SmithClient, args: argparse.Namespace) -> int:
653+
data = client.execute_ci_artifacts_grep(
654+
remote_or_provider=_selected_target(args),
655+
project=getattr(args, "project", None),
656+
repo=getattr(args, "repo", None),
657+
pipeline_id=args.id,
658+
job_id=args.job_id,
659+
pattern=args.pattern,
660+
path=getattr(args, "path", None),
661+
glob=getattr(args, "glob", None),
662+
output_mode=args.output_mode,
663+
case_insensitive=not args.case_sensitive,
664+
context_lines=args.context_lines,
665+
from_line=args.from_line,
666+
to_line=args.to_line,
667+
reverse=getattr(args, "reverse", False),
668+
)
669+
return _emit_success(
670+
args=args,
671+
command=args.command_id,
672+
data=data,
673+
partial=_is_partial_result(data),
674+
)
675+
676+
636677
def handle_work_get(client: SmithClient, args: argparse.Namespace) -> int:
637678
request_kwargs: dict[str, Any] = {
638679
"remote_or_provider": _selected_target(args),

src/smith/cli/parser.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from smith.cli.handlers import (
88
_csv_list,
99
handle_cache_clean,
10+
handle_ci_artifacts_grep,
11+
handle_ci_artifacts_list,
1012
handle_ci_grep,
1113
handle_ci_list,
1214
handle_code_grep,
@@ -183,6 +185,32 @@ def _add_ci_grep_options(parser: argparse.ArgumentParser) -> None:
183185
parser.add_argument("--case-sensitive", action="store_true")
184186

185187

188+
def _add_artifact_grep_options(parser: argparse.ArgumentParser) -> None:
189+
parser.add_argument("--path", help="Artifact path scope within the extracted archive")
190+
parser.add_argument("--glob", help="Artifact filename glob filter")
191+
parser.add_argument(
192+
"pattern",
193+
help=(
194+
'Regex pattern. Use ".*" to match all. '
195+
'Form: smith <remote> pipelines artifacts grep <scope> <pipeline_id> <job_id> "<regex>"'
196+
),
197+
)
198+
parser.add_argument(
199+
"--output-mode",
200+
choices=["content", "files_with_matches", "count"],
201+
default="content",
202+
)
203+
parser.add_argument("--context-lines", type=int, default=3)
204+
parser.add_argument("--from-line", type=int)
205+
parser.add_argument("--to-line", type=int)
206+
parser.add_argument(
207+
"--reverse",
208+
action="store_true",
209+
help="Emit matches in reverse order so the most recent hits appear first.",
210+
)
211+
parser.add_argument("--case-sensitive", action="store_true")
212+
213+
186214
def _add_work_search_filters(parser: argparse.ArgumentParser, *, include_area: bool = True) -> None:
187215
if include_area:
188216
parser.add_argument("--area")
@@ -549,6 +577,56 @@ def _add_remote_pipelines_group(remote_subparsers: Any, *, remote: RemoteConfig)
549577
_add_output_format(pipelines_grep)
550578
_set_handler(pipelines_grep, handle_ci_grep, "pipelines.grep", primary_path="pipelines grep")
551579

580+
if remote.provider == "gitlab":
581+
pipelines_artifacts = _add_parser(
582+
pipelines_sub,
583+
"artifacts",
584+
help_text="List and grep GitLab job artifacts",
585+
)
586+
pipelines_artifacts_sub = pipelines_artifacts.add_subparsers(
587+
dest="pipelines_artifacts_action",
588+
required=True,
589+
)
590+
591+
pipelines_artifacts_list = _add_parser(
592+
pipelines_artifacts_sub,
593+
"list",
594+
help_text="List artifact paths for a job",
595+
)
596+
_add_pipeline_positional_args(
597+
pipelines_artifacts_list,
598+
remote=remote,
599+
id_label=id_label,
600+
)
601+
pipelines_artifacts_list.add_argument("job_id", type=int, help="Job ID")
602+
_add_output_format(pipelines_artifacts_list)
603+
_set_handler(
604+
pipelines_artifacts_list,
605+
handle_ci_artifacts_list,
606+
"pipelines.artifacts.list",
607+
primary_path="pipelines artifacts list",
608+
)
609+
610+
pipelines_artifacts_grep = _add_parser(
611+
pipelines_artifacts_sub,
612+
"grep",
613+
help_text="Search extracted GitLab job artifacts",
614+
)
615+
_add_pipeline_positional_args(
616+
pipelines_artifacts_grep,
617+
remote=remote,
618+
id_label=id_label,
619+
)
620+
pipelines_artifacts_grep.add_argument("job_id", type=int, help="Job ID")
621+
_add_artifact_grep_options(pipelines_artifacts_grep)
622+
_add_output_format(pipelines_artifacts_grep)
623+
_set_handler(
624+
pipelines_artifacts_grep,
625+
handle_ci_artifacts_grep,
626+
"pipelines.artifacts.grep",
627+
primary_path="pipelines artifacts grep",
628+
)
629+
552630

553631
def _add_remote_stories_group(remote_subparsers: Any, *, remote: RemoteConfig) -> None:
554632
stories = _add_parser(remote_subparsers, "stories", help_text="Get, search, and get mine")

src/smith/client.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ def _require_single_target(remote_or_provider: str, *, command: str) -> str:
8080
raise ValueError(f"{command} does not support target 'all'. Use a configured remote name.")
8181
return target
8282

83+
def _require_gitlab_target(self, remote_or_provider: str, *, command: str) -> str:
84+
target = self._require_single_target(remote_or_provider, command=command)
85+
remotes = self._resolve_remotes(target)
86+
if not remotes:
87+
raise ValueError(f"No enabled remote found for '{target}'")
88+
if len(remotes) != 1 or remotes[0].provider != "gitlab":
89+
raise ValueError(f"{command} is only supported for GitLab remotes.")
90+
return target
91+
8392
def _get_provider_for_remote(self, remote: RemoteConfig) -> BaseProvider:
8493
if remote.name in self._provider_cache:
8594
return self._provider_cache[remote.name]
@@ -725,6 +734,74 @@ def execute_ci_grep(
725734
},
726735
)
727736

737+
def execute_ci_artifacts_list(
738+
self,
739+
*,
740+
remote_or_provider: str,
741+
project: str | None,
742+
repo: str | None,
743+
pipeline_id: int,
744+
job_id: int,
745+
) -> dict[str, Any]:
746+
target = self._require_gitlab_target(
747+
remote_or_provider,
748+
command="pipelines.artifacts.list",
749+
)
750+
effective_repo = repo or project
751+
return self._fanout(
752+
remote_or_provider=target,
753+
operations={
754+
"gitlab": lambda r: self._gitlab_provider(r).list_job_artifacts(
755+
repo=str(effective_repo),
756+
pipeline_id=pipeline_id,
757+
job_id=job_id,
758+
),
759+
},
760+
)
761+
762+
def execute_ci_artifacts_grep(
763+
self,
764+
*,
765+
remote_or_provider: str,
766+
project: str | None,
767+
repo: str | None,
768+
pipeline_id: int,
769+
job_id: int,
770+
pattern: str | None,
771+
path: str | None,
772+
glob: str | None,
773+
output_mode: Literal["content", "files_with_matches", "count"],
774+
case_insensitive: bool,
775+
context_lines: int | None,
776+
from_line: int | None,
777+
to_line: int | None,
778+
reverse: bool = False,
779+
) -> dict[str, Any]:
780+
target = self._require_gitlab_target(
781+
remote_or_provider,
782+
command="pipelines.artifacts.grep",
783+
)
784+
effective_repo = repo or project
785+
return self._fanout(
786+
remote_or_provider=target,
787+
operations={
788+
"gitlab": lambda r: self._gitlab_provider(r).grep_job_artifacts(
789+
repo=str(effective_repo),
790+
pipeline_id=pipeline_id,
791+
job_id=job_id,
792+
pattern=pattern,
793+
path=path,
794+
glob=glob,
795+
output_mode=output_mode,
796+
case_insensitive=case_insensitive,
797+
context_lines=context_lines,
798+
from_line=from_line,
799+
to_line=to_line,
800+
reverse=reverse,
801+
),
802+
},
803+
)
804+
728805
def execute_work_get(
729806
self,
730807
*,

src/smith/formatting.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,11 @@ def _render_grep(data: Any) -> str:
239239
return text
240240

241241

242+
def _render_artifacts_list(data: Any) -> str:
243+
paths = data.get("paths", []) if isinstance(data, dict) else []
244+
return "\n".join(str(path) for path in paths if str(path).strip())
245+
246+
242247
def _render_cache_clean(data: Any) -> str:
243248
cleaned = data.get("cleaned", []) if isinstance(data, dict) else []
244249
missing = data.get("missing", []) if isinstance(data, dict) else []
@@ -758,6 +763,8 @@ def _render_needs(needs: Any, *, name_to_id: dict[str, Any]) -> str:
758763
"cache.clean": _render_cache_clean,
759764
"pipelines.list": _render_pipelines_list,
760765
"pipelines.grep": _render_grep,
766+
"pipelines.artifacts.list": _render_artifacts_list,
767+
"pipelines.artifacts.grep": _render_grep,
761768
"prs.search": _render_pr_list,
762769
"prs.list": _render_pr_list,
763770
"prs.get": _render_pr_get,
@@ -805,7 +812,7 @@ def _render_remote_grouped(command: str, payload: dict[str, Any]) -> str:
805812
lines.append(rendered)
806813

807814
warnings = _visible_remote_warnings(command, remote_data, entry.get("warnings") or [])
808-
if command not in {"code.grep", "pipelines.grep"}:
815+
if command not in {"code.grep", "pipelines.grep", "pipelines.artifacts.grep"}:
809816
for warning in warnings:
810817
lines.append(f"warning: {warning}")
811818
return "\n".join(lines).rstrip()
@@ -851,7 +858,7 @@ def _render_remote_grouped(command: str, payload: dict[str, Any]) -> str:
851858
output_lines.append(rendered)
852859

853860
warnings = _visible_remote_warnings(command, remote_data, entry.get("warnings") or [])
854-
if command not in {"code.grep", "pipelines.grep"}:
861+
if command not in {"code.grep", "pipelines.grep", "pipelines.artifacts.grep"}:
855862
for warning in warnings:
856863
output_lines.append(f"warning: {warning}")
857864
output_lines.append("")

0 commit comments

Comments
 (0)