glab-cli-extras 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- glab_cli_extras-0.1.0/.editorconfig +20 -0
- glab_cli_extras-0.1.0/.github/workflows/ci.yml +47 -0
- glab_cli_extras-0.1.0/.github/workflows/publish.yml +44 -0
- glab_cli_extras-0.1.0/.gitignore +10 -0
- glab_cli_extras-0.1.0/.python-version +1 -0
- glab_cli_extras-0.1.0/LICENSE +21 -0
- glab_cli_extras-0.1.0/PKG-INFO +230 -0
- glab_cli_extras-0.1.0/README.md +207 -0
- glab_cli_extras-0.1.0/pyproject.toml +74 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/__init__.py +3 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/artifacts/__init__.py +0 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/artifacts/job_logs.py +95 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/artifacts/threads.py +147 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/cli/__init__.py +0 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/cli/watch.py +426 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/daemon/__init__.py +0 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/daemon/__main__.py +10 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/daemon/engine.py +491 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/daemon/lifecycle.py +186 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/daemon/locks.py +85 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/daemon/scheduler.py +52 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/daemon/server.py +79 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/daemon/transport.py +185 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/events.py +165 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/gitlab/__init__.py +0 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/gitlab/budget.py +135 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/gitlab/client.py +225 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/gitlab/glab.py +175 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/gitlab/queries/__init__.py +11 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/gitlab/queries/branch_pipelines.graphql +8 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/gitlab/queries/mr.graphql +42 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/gitlab/queries/pipeline.graphql +38 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/gitlab/refs.py +151 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/paths.py +79 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/py.typed +0 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/resources/__init__.py +19 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/resources/base.py +146 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/resources/branch.py +265 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/resources/mr.py +445 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/resources/mr_list.py +150 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/resources/pipeline.py +319 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/serde.py +75 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/watch/__init__.py +0 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/watch/changelog.py +86 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/watch/claude.py +193 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/watch/client.py +316 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/watch/sinks.py +83 -0
- glab_cli_extras-0.1.0/src/glab_cli_extras/watch/streams.py +80 -0
- glab_cli_extras-0.1.0/tests/conftest.py +50 -0
- glab_cli_extras-0.1.0/tests/fakeglab.py +170 -0
- glab_cli_extras-0.1.0/tests/shapes.py +242 -0
- glab_cli_extras-0.1.0/tests/test_budget_scheduler.py +75 -0
- glab_cli_extras-0.1.0/tests/test_claude_stream.py +257 -0
- glab_cli_extras-0.1.0/tests/test_cli.py +48 -0
- glab_cli_extras-0.1.0/tests/test_daemon.py +262 -0
- glab_cli_extras-0.1.0/tests/test_engine.py +207 -0
- glab_cli_extras-0.1.0/tests/test_events_filter.py +64 -0
- glab_cli_extras-0.1.0/tests/test_gitlab_client.py +122 -0
- glab_cli_extras-0.1.0/tests/test_locks.py +45 -0
- glab_cli_extras-0.1.0/tests/test_mr.py +187 -0
- glab_cli_extras-0.1.0/tests/test_mr_list_branch.py +182 -0
- glab_cli_extras-0.1.0/tests/test_pipeline.py +165 -0
- glab_cli_extras-0.1.0/tests/test_refs_serde_artifacts.py +102 -0
- glab_cli_extras-0.1.0/tests/test_watcher.py +113 -0
- glab_cli_extras-0.1.0/uv.lock +537 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
root = true
|
|
2
|
+
|
|
3
|
+
[*]
|
|
4
|
+
charset = utf-8
|
|
5
|
+
end_of_line = lf
|
|
6
|
+
insert_final_newline = true
|
|
7
|
+
trim_trailing_whitespace = true
|
|
8
|
+
indent_style = space
|
|
9
|
+
|
|
10
|
+
[*.py]
|
|
11
|
+
indent_size = 4
|
|
12
|
+
|
|
13
|
+
[*.{toml,yml,yaml,json}]
|
|
14
|
+
indent_size = 2
|
|
15
|
+
|
|
16
|
+
[*.md]
|
|
17
|
+
trim_trailing_whitespace = false
|
|
18
|
+
|
|
19
|
+
[Makefile]
|
|
20
|
+
indent_style = tab
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master, main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
lint:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v5
|
|
13
|
+
- uses: astral-sh/setup-uv@v7
|
|
14
|
+
with:
|
|
15
|
+
enable-cache: true
|
|
16
|
+
- run: uv sync --locked
|
|
17
|
+
- run: uv run ruff check --output-format github
|
|
18
|
+
- run: uv run ruff format --check
|
|
19
|
+
- run: uv run mypy
|
|
20
|
+
|
|
21
|
+
test:
|
|
22
|
+
strategy:
|
|
23
|
+
fail-fast: false
|
|
24
|
+
matrix:
|
|
25
|
+
os: [ubuntu-latest, macos-latest]
|
|
26
|
+
python: ["3.11", "3.13"]
|
|
27
|
+
runs-on: ${{ matrix.os }}
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v5
|
|
30
|
+
- uses: astral-sh/setup-uv@v7
|
|
31
|
+
with:
|
|
32
|
+
enable-cache: true
|
|
33
|
+
python-version: ${{ matrix.python }}
|
|
34
|
+
- run: uv sync --locked
|
|
35
|
+
- run: uv run pytest
|
|
36
|
+
|
|
37
|
+
# Windows gets "the daemon and the stdout stream work": the lock, the TCP transport, spawning and the CLI.
|
|
38
|
+
test-windows-daemon:
|
|
39
|
+
runs-on: windows-latest
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v5
|
|
42
|
+
- uses: astral-sh/setup-uv@v7
|
|
43
|
+
with:
|
|
44
|
+
enable-cache: true
|
|
45
|
+
python-version: "3.13"
|
|
46
|
+
- run: uv sync --locked
|
|
47
|
+
- run: uv run pytest tests/test_locks.py tests/test_daemon.py
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
name: Build
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
with:
|
|
15
|
+
fetch-depth: 0
|
|
16
|
+
|
|
17
|
+
- uses: astral-sh/setup-uv@v7
|
|
18
|
+
with:
|
|
19
|
+
enable-cache: true
|
|
20
|
+
|
|
21
|
+
- name: Build package
|
|
22
|
+
run: uv build
|
|
23
|
+
|
|
24
|
+
- uses: actions/upload-artifact@v4
|
|
25
|
+
with:
|
|
26
|
+
name: dist
|
|
27
|
+
path: dist/
|
|
28
|
+
|
|
29
|
+
publish:
|
|
30
|
+
name: Publish
|
|
31
|
+
needs: build
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
environment:
|
|
34
|
+
name: pypi
|
|
35
|
+
url: https://pypi.org/p/glab-cli-extras
|
|
36
|
+
permissions:
|
|
37
|
+
id-token: write
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/download-artifact@v4
|
|
40
|
+
with:
|
|
41
|
+
name: dist
|
|
42
|
+
path: dist/
|
|
43
|
+
|
|
44
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.11
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Filip Procházka
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: glab-cli-extras
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Extra commands for the GitLab CLI (glab): glab-watch reports changes to merge requests, pipelines and branches.
|
|
5
|
+
Project-URL: Homepage, https://github.com/fprochazka/glab-cli-extras
|
|
6
|
+
Project-URL: Repository, https://github.com/fprochazka/glab-cli-extras
|
|
7
|
+
Project-URL: Issues, https://github.com/fprochazka/glab-cli-extras/issues
|
|
8
|
+
Author-email: Filip Procházka <dev@fprochazka.cz>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: cc-blackbox<0.3,>=0.2
|
|
20
|
+
Requires-Dist: rich>=14
|
|
21
|
+
Requires-Dist: typer>=0.15
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# glab-cli-extras
|
|
25
|
+
|
|
26
|
+
Extra commands for the [GitLab CLI](https://gitlab.com/gitlab-org/cli) (`glab`). Version 0.1 has one command, `glab-watch`: it watches a merge request, a pipeline, all merge requests of a project, or a branch, and reports every change as one JSON event per line. It is built for coding agents that wait on CI and review, and it works just as well in a terminal or a script.
|
|
27
|
+
|
|
28
|
+
Every GitLab request goes through `glab api`, so `glab` handles authentication, hosts, OAuth refresh, proxies and certificates. `glab-watch` never reads or stores a token. A shared per-user daemon does the polling, so ten watchers cost about as much API budget as one.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
You need Python 3.11 or later, [uv](https://docs.astral.sh/uv/), and `glab` logged in to your GitLab host (`glab auth status`).
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv tool install glab-cli-extras
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
From git, for the latest unreleased changes:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv tool install git+https://github.com/fprochazka/glab-cli-extras
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
From a local checkout:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv tool install --editable /path/to/glab-cli-extras
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
glab-watch mr group/project!123 # a merge request; also !123 or 123 with the repo from the cwd, or the MR URL
|
|
54
|
+
glab-watch pipeline group/project!4567 # a pipeline and its downstream pipelines; also the pipeline URL
|
|
55
|
+
glab-watch mrs group/project # every merge request of a project
|
|
56
|
+
glab-watch branch group/project main # a branch; the repo is optional: glab-watch branch main
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`<repo>` means the same as it does to glab: `group/project`, `group/sub/project`, `host/group/project`, a project URL, or nothing for the repository of the current directory. `glab-watch` asks `glab repo view` to resolve it, which costs one request at start. A URL costs nothing.
|
|
60
|
+
|
|
61
|
+
A watcher runs until its resource ends (an MR is merged or closed, a pipeline finishes), until `--until` or `--timeout` fires, or until it gets SIGTERM or Ctrl-C. `mrs` and `branch` run until stopped. The last event is always `watch_ended` with a `reason`.
|
|
62
|
+
|
|
63
|
+
### Options
|
|
64
|
+
|
|
65
|
+
All commands:
|
|
66
|
+
|
|
67
|
+
- `--events=<kinds or groups>`: report only these. Comma-separated, repeatable. The default is every event the resource tracks.
|
|
68
|
+
- `--exclude-events=<kinds or groups>`: do not report these.
|
|
69
|
+
- `--until=<kind>`: stop after the first event of this kind, for example `--until=finished`.
|
|
70
|
+
- `--timeout=<duration>`: stop after `90s`, `30m`, `2h` or `1h30m`.
|
|
71
|
+
- `--events-stream=auto|stdout|claude-message`: where events go. See [Claude Code](#claude-code).
|
|
72
|
+
- `--changelog-dir=<path>`: where the changelog goes. See [Output](#output).
|
|
73
|
+
- `--message-priority=now|next|later`: for `claude-message` only. The default is `next`.
|
|
74
|
+
- `--no-daemon`: poll inside this process, with a private budget, without the shared daemon.
|
|
75
|
+
|
|
76
|
+
`mr` and `pipeline` also take `--logs=failed|all|none`: which finished jobs get their log downloaded. The default is `failed`.
|
|
77
|
+
|
|
78
|
+
The filter is checked at start. An unknown kind or group, or one the resource never produces, is an error, so a typo does not silently mean "no events". The filter also saves API work: an MR watch without `comments` never fetches threads, and one without `pipeline` never polls the head pipeline or downloads job logs.
|
|
79
|
+
|
|
80
|
+
## Events
|
|
81
|
+
|
|
82
|
+
Every event has `kind`, `resource`, `seq`, `observed_at` and `data`, and `artifacts` when it points at files:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{"kind":"job_finished","resource":"pipeline gitlab.example.com/group/project!4567","seq":7,"observed_at":"2026-11-09T10:05:03.120Z","data":{"pipeline_id":4567,"job_id":901,"name":"unit-tests","stage":"test","status":"failed","log_truncated":false},"artifacts":["/home/me/.cache/glab-cli-extras/gitlab.example.com/group/project/jobs/901-unit-tests.log"]}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`seq` numbers the events a watcher emits: it starts after the last `seq` in the changelog and only goes up, also when the daemon restarts in between.
|
|
89
|
+
|
|
90
|
+
Lifecycle events come with every resource and cannot be filtered out:
|
|
91
|
+
|
|
92
|
+
- `watch_started`: the current state, in `data.summary`.
|
|
93
|
+
- `watch_degraded`: polling has trouble: `rate_limited`, `auth`, `not_found` or `error`, with a message. It is sent once per kind of trouble, not on every failed poll.
|
|
94
|
+
- `watch_ended`: with a `reason`: `merged`, `closed`, `finished`, `not_found`, `until`, `timeout`, `session_gone`, or the signal.
|
|
95
|
+
|
|
96
|
+
`mr`:
|
|
97
|
+
|
|
98
|
+
- `state_changed` (`from`, `to`, and `reopened: true` for closed to opened), `draft_changed`, `title_changed`, `description_changed`
|
|
99
|
+
- `pushed`: `from`, `to`, `forced`. A force-push is detected with one `merge_base` request per push.
|
|
100
|
+
- `merge_status_changed`: GitLab's transient `checking`, `unchecked` and `preparing` are skipped.
|
|
101
|
+
- `approvals_changed`, `reviewers_changed`
|
|
102
|
+
- `head_pipeline_changed`, plus every `pipeline` event of the head pipeline, with `pipeline_id` in `data`
|
|
103
|
+
- `thread_created`, `thread_updated`, `thread_resolved`, `thread_unresolved`, each with the thread's file in `artifacts`
|
|
104
|
+
|
|
105
|
+
`pipeline`: `status_changed`, `job_started`, `job_finished` (with the log in `artifacts` when `--logs` asks for it), `downstream_created`, `finished`.
|
|
106
|
+
|
|
107
|
+
`mrs`: `mr_opened`, `mr_updated`, `mr_merged`, `mr_closed`, `mr_reopened`.
|
|
108
|
+
|
|
109
|
+
`branch`: `pushed`, `force_pushed`, `created`, `deleted`, `pipeline_created`, `pipeline_finished`. A push whose head commit is a merge commit has `merge_commit: true` and, when GitLab knows it, `merged_mr_iid`.
|
|
110
|
+
|
|
111
|
+
Groups for `--events` and `--exclude-events`:
|
|
112
|
+
|
|
113
|
+
| Group | Kinds |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `status` | `state_changed`, `draft_changed`, `merge_status_changed`, `approvals_changed`, `reviewers_changed`, `title_changed`, `description_changed`, and all `mr_*` |
|
|
116
|
+
| `comments` | `thread_*` |
|
|
117
|
+
| `push` | `pushed`, `force_pushed`, `created`, `deleted` |
|
|
118
|
+
| `pipeline` | `head_pipeline_changed`, `status_changed`, `job_*`, `downstream_created`, `finished`, `pipeline_created`, `pipeline_finished` |
|
|
119
|
+
| `lifecycle` | `watch_started`, `watch_degraded`, `watch_ended`; always on |
|
|
120
|
+
|
|
121
|
+
### Artifacts
|
|
122
|
+
|
|
123
|
+
Job logs and thread files go to a cache shared by all watchers: `~/.cache/glab-cli-extras/<host>/<project>/` on Linux, `~/Library/Caches/glab-cli-extras/` on macOS, `%LOCALAPPDATA%\glab-cli-extras\cache\` on Windows. Two watchers of the same MR download each thread once.
|
|
124
|
+
|
|
125
|
+
- A job log is downloaded once, a few seconds after the job ends. On GitLab 19.0 and later a log above 500 KB is read as its last 500 KB only, and the file starts with a `[glab-watch: last …]` line.
|
|
126
|
+
- A thread file is `mr-<iid>/threads/<created>-<author>-<id>.md`, in the format glab-discussion uses. It is rewritten only when the thread changed: a new reply, an edit, a resolve.
|
|
127
|
+
|
|
128
|
+
## Claude Code
|
|
129
|
+
|
|
130
|
+
Start the watcher as a background Bash task from a Claude Code session:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
glab-watch mr group/project!123 --exclude-events=comments
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
With the default `--events-stream=auto`, a watcher that runs under a Claude Code session which accepts messages sends its events to that session as messages, through the session's messaging socket (`CLAUDE_CODE_MESSAGING_SOCKET`). The session sees each batch as a message from `glab-watch mr group/project!123`:
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
<glab-cli-extras-watcher resource="mr gitlab.example.com/group/project!123" seq="18-19" changelog="/tmp/claude-1000/…/scratchpad/glab-watch/gitlab.example.com/group/project/mr-123/events.jsonl">
|
|
140
|
+
{"kind":"head_pipeline_changed","seq":18,"data":{"pipeline_id":456,"status":"running","previous_pipeline_id":455}}
|
|
141
|
+
{"kind":"thread_created","seq":19,"data":{"discussion_id":"6a1f…","author":"reviewer-one","resolvable":true,"resolved":false,"notes":1},"artifacts":["/home/me/.cache/glab-cli-extras/…/threads/2026-11-09T09-30-reviewer-one-6a1f….md"]}
|
|
142
|
+
</glab-cli-extras-watcher>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- **Batching.** Events that arrive close together go out as one message: the first event of a burst waits up to 3 s for the rest. A batch above 64 KB is sent as a count per kind; the changelog has the full events.
|
|
146
|
+
- **Priority.** `--message-priority=next` (the default) delivers into the session's running turn, at its next tool round. `later` waits until the session is idle and then starts a new turn. `now` is the most urgent.
|
|
147
|
+
- **Rate limits.** Each watcher sends as `glab-watch:<pid>`, and keeps below the session's per-sender limit; messages it cannot send yet are merged into the next one.
|
|
148
|
+
- **Changelog.** Under a Claude Code session the changelog goes to `<session scratchpad>/glab-watch/`, in both stream modes.
|
|
149
|
+
- **Lifetime.** A `claude-message` watcher exits when its session is gone: the socket refuses the message, or the session's record is no longer live. Watchers never outlive their session.
|
|
150
|
+
- **Subagents.** A watcher started from a subagent's Bash messages the main session. Claude Code has no route to a subagent.
|
|
151
|
+
- **Best effort.** The session may drop a message under its own rate limit, or hold it until its user approves it, which a session in bypass-permissions mode does. The changelog always has every event.
|
|
152
|
+
|
|
153
|
+
`--events-stream=stdout` keeps the events in the task output instead. `--events-stream=claude-message` fails at start when no session accepts messages.
|
|
154
|
+
|
|
155
|
+
When the session's messaging does not match what the cc-blackbox SDK speaks, for example after a Claude Code update, `auto` uses stdout, and the header says why on a `glab-watch: stream fallback:` line. If that happens while the watcher runs, it prints the same line and sends the rest of the events to stdout.
|
|
156
|
+
|
|
157
|
+
## Output
|
|
158
|
+
|
|
159
|
+
On start, every watcher prints a header. Each line starts with `glab-watch: `:
|
|
160
|
+
|
|
161
|
+
```text
|
|
162
|
+
glab-watch: resource: mr gitlab.example.com/group/project!123
|
|
163
|
+
glab-watch: stream: stdout (one JSON event per line)
|
|
164
|
+
glab-watch: changelog: /tmp/glab-watch-1000/gitlab.example.com/group/project/mr-123/events.jsonl
|
|
165
|
+
glab-watch: events: state_changed,draft_changed,pushed,… (always: watch_started,watch_degraded,watch_ended)
|
|
166
|
+
glab-watch: ignored: thread_created,thread_updated,thread_resolved,thread_unresolved
|
|
167
|
+
glab-watch: logs: failed
|
|
168
|
+
glab-watch: daemon: pid 4242, endpoint /run/user/1000/glab-cli-extras/daemon-v1.sock, log /run/user/1000/glab-cli-extras/daemon-v1.log
|
|
169
|
+
glab-watch: restart with: glab-watch mr https://gitlab.example.com/group/project/-/merge_requests/123 --events=<kinds|groups> --exclude-events=<kinds|groups> --logs=failed|all|none (groups: status, comments, push, pipeline)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The header and the other status lines (`ended: …`, errors) always go to stderr, so stdout carries only events. Claude Code puts a background task's stderr into the task output next to its stdout, so an agent sees both. A status line never starts with `{`: in merged output, every line that starts with `{` is an event.
|
|
173
|
+
|
|
174
|
+
The changelog keeps every reported event even when nobody read the stream: `<dir>/<host>/<project>/<kind>-<id>/events.jsonl`, plus `state.json` with the last snapshot. The default `<dir>` is `<tempdir>/glab-watch-<uid>`.
|
|
175
|
+
|
|
176
|
+
## The daemon
|
|
177
|
+
|
|
178
|
+
The first watcher starts the daemon, `python -m glab_cli_extras.daemon`, as a detached process. It exits 30 s after its last watcher is gone. You never have to start it.
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
glab-watch daemon status # subscriptions, resources, request counts and budget per host; --json for everything
|
|
182
|
+
glab-watch daemon logs -n 100 # the end of the daemon's log
|
|
183
|
+
glab-watch daemon stop # running watchers start a new daemon on their next poll and carry on
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
- **One daemon per user.** It holds an OS lock on `daemon-v1.lock` in its state dir (`$XDG_RUNTIME_DIR/glab-cli-extras/` on Linux, `$TMPDIR/glab-cli-extras/` on macOS, `%LOCALAPPDATA%\glab-cli-extras\run\` on Windows). Watchers that start at the same moment start one daemon, not one each.
|
|
187
|
+
- **Leases.** Each watcher polls the daemon every 5 s, which renews its subscriptions. A watcher that dies stops costing anything 20 s later.
|
|
188
|
+
- **Restarts.** If the daemon goes away, watchers start a new one and subscribe again. The new daemon compares GitLab's state with the snapshot each watcher kept, so a change made in between is still reported, and nothing is reported twice.
|
|
189
|
+
- **Budget.** The daemon paces each host on GitLab's `RateLimit-Remaining` and `RateLimit-Reset` headers and uses at most half of what is left. A host without those headers gets 60 requests a minute. When the budget is short, every poll interval grows by the same factor. A 429 pauses the whole host for its `Retry-After`.
|
|
190
|
+
- **Cheap probes.** Pipelines are polled through GitLab's GraphQL ETag cache: an unchanged pipeline answers with a 304 before any query runs. Base intervals: pipeline 5 s while running, MR 30 s (15 s on hosts with a high rate limit), branch pipelines 10 s, branch pushes 30 s, `mrs` 60 s.
|
|
191
|
+
- **Sharing.** Watchers of the same resource share one poller. All watched branches of a project share one push-event request.
|
|
192
|
+
|
|
193
|
+
## Platform support
|
|
194
|
+
|
|
195
|
+
- Linux and macOS: everything. The daemon listens on a Unix socket.
|
|
196
|
+
- Windows: the daemon and the stdout stream. The daemon listens on a loopback TCP port, and every request carries a random token from a file only the user can read. The `claude-message` stream is not available.
|
|
197
|
+
|
|
198
|
+
## Limitations
|
|
199
|
+
|
|
200
|
+
- **No `claude-message` on Windows.** The SDK has no Windows transport yet; `auto` picks stdout there.
|
|
201
|
+
- **One glab process per request.** Starting `glab` costs tens of milliseconds. At these request rates, a few per second at most, that does not matter.
|
|
202
|
+
- **The free ETag 304 is not documented by GitLab.** If GitLab starts to count those requests, pipeline watching gets slower under the budget, not more expensive.
|
|
203
|
+
- **Branch pipeline probes are cheap only on quiet projects.** GitLab touches the ETag key of a project's pipelines on any pipeline change in the project. In a busy monorepo nearly every 10 s probe is a full request.
|
|
204
|
+
- **`mr_updated` is best effort.** GitLab does not move an MR's `updated_at` when it was touched less than a minute before, so a second change within that minute shows up only with the next change.
|
|
205
|
+
- **Job logs of large jobs need GitLab 19.0** for the tail-only download. Older versions download the whole log.
|
|
206
|
+
- **Thread files have no bot marker** and use usernames, not display names, because that would cost a user lookup per author.
|
|
207
|
+
- **Deeper downstream pipelines** (a child of a child) are not watched. Their changes do reach the watched pipeline's ETag, but their jobs are not reported.
|
|
208
|
+
- **SSH host aliases** in git remotes are resolved by glab, as glab resolves them. A remote glab cannot map to a configured host fails at start.
|
|
209
|
+
|
|
210
|
+
## Development
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
uv sync
|
|
214
|
+
uv run ruff check
|
|
215
|
+
uv run ruff format --check
|
|
216
|
+
uv run mypy
|
|
217
|
+
uv run pytest
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
To run against a local checkout of [cc-blackbox](https://github.com/fprochazka/cc-blackbox) instead of its release, for example while changing both, layer it over the locked version for one command. This leaves `pyproject.toml` and `uv.lock` alone:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
uv run --with-editable ../cc-blackbox pytest
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The tests need no network and no glab login. A fake `glab` (`tests/fakeglab.py`) answers with the response shapes recorded from a GitLab 19.5 instance, in process for the pollers and as a real executable on `PATH` for the daemon tests, which start real daemons in temporary dirs.
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# glab-cli-extras
|
|
2
|
+
|
|
3
|
+
Extra commands for the [GitLab CLI](https://gitlab.com/gitlab-org/cli) (`glab`). Version 0.1 has one command, `glab-watch`: it watches a merge request, a pipeline, all merge requests of a project, or a branch, and reports every change as one JSON event per line. It is built for coding agents that wait on CI and review, and it works just as well in a terminal or a script.
|
|
4
|
+
|
|
5
|
+
Every GitLab request goes through `glab api`, so `glab` handles authentication, hosts, OAuth refresh, proxies and certificates. `glab-watch` never reads or stores a token. A shared per-user daemon does the polling, so ten watchers cost about as much API budget as one.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
You need Python 3.11 or later, [uv](https://docs.astral.sh/uv/), and `glab` logged in to your GitLab host (`glab auth status`).
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv tool install glab-cli-extras
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
From git, for the latest unreleased changes:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv tool install git+https://github.com/fprochazka/glab-cli-extras
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
From a local checkout:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv tool install --editable /path/to/glab-cli-extras
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
glab-watch mr group/project!123 # a merge request; also !123 or 123 with the repo from the cwd, or the MR URL
|
|
31
|
+
glab-watch pipeline group/project!4567 # a pipeline and its downstream pipelines; also the pipeline URL
|
|
32
|
+
glab-watch mrs group/project # every merge request of a project
|
|
33
|
+
glab-watch branch group/project main # a branch; the repo is optional: glab-watch branch main
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`<repo>` means the same as it does to glab: `group/project`, `group/sub/project`, `host/group/project`, a project URL, or nothing for the repository of the current directory. `glab-watch` asks `glab repo view` to resolve it, which costs one request at start. A URL costs nothing.
|
|
37
|
+
|
|
38
|
+
A watcher runs until its resource ends (an MR is merged or closed, a pipeline finishes), until `--until` or `--timeout` fires, or until it gets SIGTERM or Ctrl-C. `mrs` and `branch` run until stopped. The last event is always `watch_ended` with a `reason`.
|
|
39
|
+
|
|
40
|
+
### Options
|
|
41
|
+
|
|
42
|
+
All commands:
|
|
43
|
+
|
|
44
|
+
- `--events=<kinds or groups>`: report only these. Comma-separated, repeatable. The default is every event the resource tracks.
|
|
45
|
+
- `--exclude-events=<kinds or groups>`: do not report these.
|
|
46
|
+
- `--until=<kind>`: stop after the first event of this kind, for example `--until=finished`.
|
|
47
|
+
- `--timeout=<duration>`: stop after `90s`, `30m`, `2h` or `1h30m`.
|
|
48
|
+
- `--events-stream=auto|stdout|claude-message`: where events go. See [Claude Code](#claude-code).
|
|
49
|
+
- `--changelog-dir=<path>`: where the changelog goes. See [Output](#output).
|
|
50
|
+
- `--message-priority=now|next|later`: for `claude-message` only. The default is `next`.
|
|
51
|
+
- `--no-daemon`: poll inside this process, with a private budget, without the shared daemon.
|
|
52
|
+
|
|
53
|
+
`mr` and `pipeline` also take `--logs=failed|all|none`: which finished jobs get their log downloaded. The default is `failed`.
|
|
54
|
+
|
|
55
|
+
The filter is checked at start. An unknown kind or group, or one the resource never produces, is an error, so a typo does not silently mean "no events". The filter also saves API work: an MR watch without `comments` never fetches threads, and one without `pipeline` never polls the head pipeline or downloads job logs.
|
|
56
|
+
|
|
57
|
+
## Events
|
|
58
|
+
|
|
59
|
+
Every event has `kind`, `resource`, `seq`, `observed_at` and `data`, and `artifacts` when it points at files:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{"kind":"job_finished","resource":"pipeline gitlab.example.com/group/project!4567","seq":7,"observed_at":"2026-11-09T10:05:03.120Z","data":{"pipeline_id":4567,"job_id":901,"name":"unit-tests","stage":"test","status":"failed","log_truncated":false},"artifacts":["/home/me/.cache/glab-cli-extras/gitlab.example.com/group/project/jobs/901-unit-tests.log"]}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`seq` numbers the events a watcher emits: it starts after the last `seq` in the changelog and only goes up, also when the daemon restarts in between.
|
|
66
|
+
|
|
67
|
+
Lifecycle events come with every resource and cannot be filtered out:
|
|
68
|
+
|
|
69
|
+
- `watch_started`: the current state, in `data.summary`.
|
|
70
|
+
- `watch_degraded`: polling has trouble: `rate_limited`, `auth`, `not_found` or `error`, with a message. It is sent once per kind of trouble, not on every failed poll.
|
|
71
|
+
- `watch_ended`: with a `reason`: `merged`, `closed`, `finished`, `not_found`, `until`, `timeout`, `session_gone`, or the signal.
|
|
72
|
+
|
|
73
|
+
`mr`:
|
|
74
|
+
|
|
75
|
+
- `state_changed` (`from`, `to`, and `reopened: true` for closed to opened), `draft_changed`, `title_changed`, `description_changed`
|
|
76
|
+
- `pushed`: `from`, `to`, `forced`. A force-push is detected with one `merge_base` request per push.
|
|
77
|
+
- `merge_status_changed`: GitLab's transient `checking`, `unchecked` and `preparing` are skipped.
|
|
78
|
+
- `approvals_changed`, `reviewers_changed`
|
|
79
|
+
- `head_pipeline_changed`, plus every `pipeline` event of the head pipeline, with `pipeline_id` in `data`
|
|
80
|
+
- `thread_created`, `thread_updated`, `thread_resolved`, `thread_unresolved`, each with the thread's file in `artifacts`
|
|
81
|
+
|
|
82
|
+
`pipeline`: `status_changed`, `job_started`, `job_finished` (with the log in `artifacts` when `--logs` asks for it), `downstream_created`, `finished`.
|
|
83
|
+
|
|
84
|
+
`mrs`: `mr_opened`, `mr_updated`, `mr_merged`, `mr_closed`, `mr_reopened`.
|
|
85
|
+
|
|
86
|
+
`branch`: `pushed`, `force_pushed`, `created`, `deleted`, `pipeline_created`, `pipeline_finished`. A push whose head commit is a merge commit has `merge_commit: true` and, when GitLab knows it, `merged_mr_iid`.
|
|
87
|
+
|
|
88
|
+
Groups for `--events` and `--exclude-events`:
|
|
89
|
+
|
|
90
|
+
| Group | Kinds |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `status` | `state_changed`, `draft_changed`, `merge_status_changed`, `approvals_changed`, `reviewers_changed`, `title_changed`, `description_changed`, and all `mr_*` |
|
|
93
|
+
| `comments` | `thread_*` |
|
|
94
|
+
| `push` | `pushed`, `force_pushed`, `created`, `deleted` |
|
|
95
|
+
| `pipeline` | `head_pipeline_changed`, `status_changed`, `job_*`, `downstream_created`, `finished`, `pipeline_created`, `pipeline_finished` |
|
|
96
|
+
| `lifecycle` | `watch_started`, `watch_degraded`, `watch_ended`; always on |
|
|
97
|
+
|
|
98
|
+
### Artifacts
|
|
99
|
+
|
|
100
|
+
Job logs and thread files go to a cache shared by all watchers: `~/.cache/glab-cli-extras/<host>/<project>/` on Linux, `~/Library/Caches/glab-cli-extras/` on macOS, `%LOCALAPPDATA%\glab-cli-extras\cache\` on Windows. Two watchers of the same MR download each thread once.
|
|
101
|
+
|
|
102
|
+
- A job log is downloaded once, a few seconds after the job ends. On GitLab 19.0 and later a log above 500 KB is read as its last 500 KB only, and the file starts with a `[glab-watch: last …]` line.
|
|
103
|
+
- A thread file is `mr-<iid>/threads/<created>-<author>-<id>.md`, in the format glab-discussion uses. It is rewritten only when the thread changed: a new reply, an edit, a resolve.
|
|
104
|
+
|
|
105
|
+
## Claude Code
|
|
106
|
+
|
|
107
|
+
Start the watcher as a background Bash task from a Claude Code session:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
glab-watch mr group/project!123 --exclude-events=comments
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
With the default `--events-stream=auto`, a watcher that runs under a Claude Code session which accepts messages sends its events to that session as messages, through the session's messaging socket (`CLAUDE_CODE_MESSAGING_SOCKET`). The session sees each batch as a message from `glab-watch mr group/project!123`:
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
<glab-cli-extras-watcher resource="mr gitlab.example.com/group/project!123" seq="18-19" changelog="/tmp/claude-1000/…/scratchpad/glab-watch/gitlab.example.com/group/project/mr-123/events.jsonl">
|
|
117
|
+
{"kind":"head_pipeline_changed","seq":18,"data":{"pipeline_id":456,"status":"running","previous_pipeline_id":455}}
|
|
118
|
+
{"kind":"thread_created","seq":19,"data":{"discussion_id":"6a1f…","author":"reviewer-one","resolvable":true,"resolved":false,"notes":1},"artifacts":["/home/me/.cache/glab-cli-extras/…/threads/2026-11-09T09-30-reviewer-one-6a1f….md"]}
|
|
119
|
+
</glab-cli-extras-watcher>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- **Batching.** Events that arrive close together go out as one message: the first event of a burst waits up to 3 s for the rest. A batch above 64 KB is sent as a count per kind; the changelog has the full events.
|
|
123
|
+
- **Priority.** `--message-priority=next` (the default) delivers into the session's running turn, at its next tool round. `later` waits until the session is idle and then starts a new turn. `now` is the most urgent.
|
|
124
|
+
- **Rate limits.** Each watcher sends as `glab-watch:<pid>`, and keeps below the session's per-sender limit; messages it cannot send yet are merged into the next one.
|
|
125
|
+
- **Changelog.** Under a Claude Code session the changelog goes to `<session scratchpad>/glab-watch/`, in both stream modes.
|
|
126
|
+
- **Lifetime.** A `claude-message` watcher exits when its session is gone: the socket refuses the message, or the session's record is no longer live. Watchers never outlive their session.
|
|
127
|
+
- **Subagents.** A watcher started from a subagent's Bash messages the main session. Claude Code has no route to a subagent.
|
|
128
|
+
- **Best effort.** The session may drop a message under its own rate limit, or hold it until its user approves it, which a session in bypass-permissions mode does. The changelog always has every event.
|
|
129
|
+
|
|
130
|
+
`--events-stream=stdout` keeps the events in the task output instead. `--events-stream=claude-message` fails at start when no session accepts messages.
|
|
131
|
+
|
|
132
|
+
When the session's messaging does not match what the cc-blackbox SDK speaks, for example after a Claude Code update, `auto` uses stdout, and the header says why on a `glab-watch: stream fallback:` line. If that happens while the watcher runs, it prints the same line and sends the rest of the events to stdout.
|
|
133
|
+
|
|
134
|
+
## Output
|
|
135
|
+
|
|
136
|
+
On start, every watcher prints a header. Each line starts with `glab-watch: `:
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
glab-watch: resource: mr gitlab.example.com/group/project!123
|
|
140
|
+
glab-watch: stream: stdout (one JSON event per line)
|
|
141
|
+
glab-watch: changelog: /tmp/glab-watch-1000/gitlab.example.com/group/project/mr-123/events.jsonl
|
|
142
|
+
glab-watch: events: state_changed,draft_changed,pushed,… (always: watch_started,watch_degraded,watch_ended)
|
|
143
|
+
glab-watch: ignored: thread_created,thread_updated,thread_resolved,thread_unresolved
|
|
144
|
+
glab-watch: logs: failed
|
|
145
|
+
glab-watch: daemon: pid 4242, endpoint /run/user/1000/glab-cli-extras/daemon-v1.sock, log /run/user/1000/glab-cli-extras/daemon-v1.log
|
|
146
|
+
glab-watch: restart with: glab-watch mr https://gitlab.example.com/group/project/-/merge_requests/123 --events=<kinds|groups> --exclude-events=<kinds|groups> --logs=failed|all|none (groups: status, comments, push, pipeline)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The header and the other status lines (`ended: …`, errors) always go to stderr, so stdout carries only events. Claude Code puts a background task's stderr into the task output next to its stdout, so an agent sees both. A status line never starts with `{`: in merged output, every line that starts with `{` is an event.
|
|
150
|
+
|
|
151
|
+
The changelog keeps every reported event even when nobody read the stream: `<dir>/<host>/<project>/<kind>-<id>/events.jsonl`, plus `state.json` with the last snapshot. The default `<dir>` is `<tempdir>/glab-watch-<uid>`.
|
|
152
|
+
|
|
153
|
+
## The daemon
|
|
154
|
+
|
|
155
|
+
The first watcher starts the daemon, `python -m glab_cli_extras.daemon`, as a detached process. It exits 30 s after its last watcher is gone. You never have to start it.
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
glab-watch daemon status # subscriptions, resources, request counts and budget per host; --json for everything
|
|
159
|
+
glab-watch daemon logs -n 100 # the end of the daemon's log
|
|
160
|
+
glab-watch daemon stop # running watchers start a new daemon on their next poll and carry on
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- **One daemon per user.** It holds an OS lock on `daemon-v1.lock` in its state dir (`$XDG_RUNTIME_DIR/glab-cli-extras/` on Linux, `$TMPDIR/glab-cli-extras/` on macOS, `%LOCALAPPDATA%\glab-cli-extras\run\` on Windows). Watchers that start at the same moment start one daemon, not one each.
|
|
164
|
+
- **Leases.** Each watcher polls the daemon every 5 s, which renews its subscriptions. A watcher that dies stops costing anything 20 s later.
|
|
165
|
+
- **Restarts.** If the daemon goes away, watchers start a new one and subscribe again. The new daemon compares GitLab's state with the snapshot each watcher kept, so a change made in between is still reported, and nothing is reported twice.
|
|
166
|
+
- **Budget.** The daemon paces each host on GitLab's `RateLimit-Remaining` and `RateLimit-Reset` headers and uses at most half of what is left. A host without those headers gets 60 requests a minute. When the budget is short, every poll interval grows by the same factor. A 429 pauses the whole host for its `Retry-After`.
|
|
167
|
+
- **Cheap probes.** Pipelines are polled through GitLab's GraphQL ETag cache: an unchanged pipeline answers with a 304 before any query runs. Base intervals: pipeline 5 s while running, MR 30 s (15 s on hosts with a high rate limit), branch pipelines 10 s, branch pushes 30 s, `mrs` 60 s.
|
|
168
|
+
- **Sharing.** Watchers of the same resource share one poller. All watched branches of a project share one push-event request.
|
|
169
|
+
|
|
170
|
+
## Platform support
|
|
171
|
+
|
|
172
|
+
- Linux and macOS: everything. The daemon listens on a Unix socket.
|
|
173
|
+
- Windows: the daemon and the stdout stream. The daemon listens on a loopback TCP port, and every request carries a random token from a file only the user can read. The `claude-message` stream is not available.
|
|
174
|
+
|
|
175
|
+
## Limitations
|
|
176
|
+
|
|
177
|
+
- **No `claude-message` on Windows.** The SDK has no Windows transport yet; `auto` picks stdout there.
|
|
178
|
+
- **One glab process per request.** Starting `glab` costs tens of milliseconds. At these request rates, a few per second at most, that does not matter.
|
|
179
|
+
- **The free ETag 304 is not documented by GitLab.** If GitLab starts to count those requests, pipeline watching gets slower under the budget, not more expensive.
|
|
180
|
+
- **Branch pipeline probes are cheap only on quiet projects.** GitLab touches the ETag key of a project's pipelines on any pipeline change in the project. In a busy monorepo nearly every 10 s probe is a full request.
|
|
181
|
+
- **`mr_updated` is best effort.** GitLab does not move an MR's `updated_at` when it was touched less than a minute before, so a second change within that minute shows up only with the next change.
|
|
182
|
+
- **Job logs of large jobs need GitLab 19.0** for the tail-only download. Older versions download the whole log.
|
|
183
|
+
- **Thread files have no bot marker** and use usernames, not display names, because that would cost a user lookup per author.
|
|
184
|
+
- **Deeper downstream pipelines** (a child of a child) are not watched. Their changes do reach the watched pipeline's ETag, but their jobs are not reported.
|
|
185
|
+
- **SSH host aliases** in git remotes are resolved by glab, as glab resolves them. A remote glab cannot map to a configured host fails at start.
|
|
186
|
+
|
|
187
|
+
## Development
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
uv sync
|
|
191
|
+
uv run ruff check
|
|
192
|
+
uv run ruff format --check
|
|
193
|
+
uv run mypy
|
|
194
|
+
uv run pytest
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
To run against a local checkout of [cc-blackbox](https://github.com/fprochazka/cc-blackbox) instead of its release, for example while changing both, layer it over the locked version for one command. This leaves `pyproject.toml` and `uv.lock` alone:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
uv run --with-editable ../cc-blackbox pytest
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The tests need no network and no glab login. A fake `glab` (`tests/fakeglab.py`) answers with the response shapes recorded from a GitLab 19.5 instance, in process for the pollers and as a real executable on `PATH` for the daemon tests, which start real daemons in temporary dirs.
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
MIT
|