queue-watch 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.
- queue_watch-0.1.0/.gitignore +7 -0
- queue_watch-0.1.0/LICENSE +21 -0
- queue_watch-0.1.0/PKG-INFO +141 -0
- queue_watch-0.1.0/README.md +114 -0
- queue_watch-0.1.0/examples/queue-status.py +64 -0
- queue_watch-0.1.0/examples/queue.json +5 -0
- queue_watch-0.1.0/examples/sources.toml +8 -0
- queue_watch-0.1.0/pyproject.toml +58 -0
- queue_watch-0.1.0/src/queue_watch/__init__.py +3 -0
- queue_watch-0.1.0/src/queue_watch/adapters/__init__.py +34 -0
- queue_watch-0.1.0/src/queue_watch/adapters/command.py +54 -0
- queue_watch-0.1.0/src/queue_watch/adapters/flock_queue.py +322 -0
- queue_watch-0.1.0/src/queue_watch/adapters/github_actions.py +164 -0
- queue_watch-0.1.0/src/queue_watch/adapters/github_merge_queue.py +141 -0
- queue_watch-0.1.0/src/queue_watch/app.py +876 -0
- queue_watch-0.1.0/src/queue_watch/changes.py +181 -0
- queue_watch-0.1.0/src/queue_watch/cli.py +129 -0
- queue_watch-0.1.0/src/queue_watch/config.py +105 -0
- queue_watch-0.1.0/src/queue_watch/discovery.py +149 -0
- queue_watch-0.1.0/src/queue_watch/model.py +188 -0
- queue_watch-0.1.0/src/queue_watch/process.py +67 -0
- queue_watch-0.1.0/src/queue_watch/widgets.py +774 -0
- queue_watch-0.1.0/tests/__snapshots__/test_snapshots/test_comfortable_density.raw +257 -0
- queue_watch-0.1.0/tests/__snapshots__/test_snapshots/test_layout[narrow].raw +195 -0
- queue_watch-0.1.0/tests/__snapshots__/test_snapshots/test_layout[short].raw +158 -0
- queue_watch-0.1.0/tests/__snapshots__/test_snapshots/test_layout[wide].raw +257 -0
- queue_watch-0.1.0/tests/test_app.py +369 -0
- queue_watch-0.1.0/tests/test_changes.py +83 -0
- queue_watch-0.1.0/tests/test_command.py +93 -0
- queue_watch-0.1.0/tests/test_contract.py +90 -0
- queue_watch-0.1.0/tests/test_discovery.py +81 -0
- queue_watch-0.1.0/tests/test_flock_queue.py +153 -0
- queue_watch-0.1.0/tests/test_github_actions.py +121 -0
- queue_watch-0.1.0/tests/test_github_merge_queue.py +79 -0
- queue_watch-0.1.0/tests/test_snapshots.py +79 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wolfgang Schoenberger
|
|
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,141 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: queue-watch
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Read-only terminal dashboard for CI runs and merge queues, with mouse support
|
|
5
|
+
Project-URL: Homepage, https://github.com/wolfiesch/queue-watch
|
|
6
|
+
Project-URL: Issues, https://github.com/wolfiesch/queue-watch/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/wolfiesch/queue-watch/releases
|
|
8
|
+
Author: Wolfgang Schoenberger
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ci,dashboard,github-actions,merge-queue,textual,tui
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
23
|
+
Classifier: Topic :: Software Development :: Testing
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Requires-Dist: textual<7,>=6
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# queue-watch
|
|
29
|
+
|
|
30
|
+
Read-only terminal dashboard for CI runs and merge queues. It joins records from every source into one row per change, so a branch's CI runs and its merge-queue entry read together, and draws each merge queue as a chain of chips in queue order. queue-watch never reruns, cancels, merges or takes a lock.
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
uv tool install queue-watch # or: pipx install queue-watch
|
|
38
|
+
uvx queue-watch # run once without installing
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires Python 3.11+. The GitHub adapters use an authenticated [GitHub CLI](https://cli.github.com/) (`gh auth login`).
|
|
42
|
+
|
|
43
|
+
## Use
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
queue-watch # inside a GitHub checkout
|
|
47
|
+
queue-watch --repo owner/repo # Actions plus the merge queue, if any
|
|
48
|
+
queue-watch --profile work # ~/.config/queue-watch/work.toml
|
|
49
|
+
queue-watch --config sources.toml # any combination of sources
|
|
50
|
+
queue-watch --repo owner/repo --once # one JSON snapshot, no TUI
|
|
51
|
+
queue-watch --theme nord --ascii # any Textual theme; ASCII glyphs only
|
|
52
|
+
queue-watch --density comfortable # title, owner and PR on a second line
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
With no flags, queue-watch looks for a `.queue-watch.toml` at the root of the current Git checkout. Without one, it watches the checkout's GitHub repository: Actions runs, plus the merge queue when the default branch has one. `--profile NAME` reads `$XDG_CONFIG_HOME/queue-watch/NAME.toml` (default `~/.config`).
|
|
56
|
+
|
|
57
|
+
The Changes tab groups changes into Needs attention, Running, Queued and Done; every view a source reports also gets its own tab. Done starts folded while any other group has changes; press enter on a group row, or click it, to fold or unfold it. Select a change or a queue chip to see its CI checks, the jobs of the most notable one, its queue position and ETA, and recent history in the inspector.
|
|
58
|
+
|
|
59
|
+
Each merge queue gets a strip above the tabs: its entries in merge order, when it should clear, merges in the last hour, and how long recent attempts took. Queue depth over the session appears as a sparkline once two fetches have answered; sources report only the current queue, so the history starts when queue-watch does. On terminals under 30 rows the entries shrink to one line each.
|
|
60
|
+
|
|
61
|
+
Keyboard: arrows to move, `1` to `9` for tabs, `/` to search (esc to clear), `o` to open the selection in a browser, `c` to copy its SHA, `d` to switch row density, `r` to refresh, `p` to pause, `q` to quit. The mouse works too: click tabs, rows, chips and the inspector buttons, and scroll with the wheel. When a source fails, its last good data stays on screen and its status pill turns stale.
|
|
62
|
+
|
|
63
|
+
### Repository configs need trust
|
|
64
|
+
|
|
65
|
+
A config can run commands and open SSH connections, so a `.queue-watch.toml` that arrives with a clone does nothing until you review it and run:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
queue-watch trust
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Trust covers the file's exact bytes (SHA-256, stored in `$XDG_STATE_HOME/queue-watch/trusted.json`). After any edit, queue-watch refuses the file and names the command to run again. Files passed with `--config` or `--profile` are yours and load without this step.
|
|
72
|
+
|
|
73
|
+
## Sources
|
|
74
|
+
|
|
75
|
+
A config file lists sources as `[[source]]` tables. `id` and `adapter` are required; `label` and `interval` (seconds) are optional; every other key is an adapter option.
|
|
76
|
+
|
|
77
|
+
```toml
|
|
78
|
+
[[source]]
|
|
79
|
+
id = "ci"
|
|
80
|
+
adapter = "github-actions"
|
|
81
|
+
repo = "owner/repo"
|
|
82
|
+
|
|
83
|
+
[[source]]
|
|
84
|
+
id = "merge-queue"
|
|
85
|
+
adapter = "github-merge-queue"
|
|
86
|
+
repo = "owner/repo"
|
|
87
|
+
branch = "main" # optional; default branch otherwise
|
|
88
|
+
|
|
89
|
+
[[source]]
|
|
90
|
+
id = "deploys"
|
|
91
|
+
adapter = "command"
|
|
92
|
+
command = ["./bin/deploy-status", "--json"]
|
|
93
|
+
timeout = 20 # optional seconds, default 25
|
|
94
|
+
|
|
95
|
+
[[source]]
|
|
96
|
+
id = "merges"
|
|
97
|
+
adapter = "flock-queue"
|
|
98
|
+
host = "build-host" # optional SSH destination
|
|
99
|
+
root = "~/src/project" # Git checkout that defines {git_dir}
|
|
100
|
+
lock = "{git_dir}/merge.lock"
|
|
101
|
+
tickets = "{git_dir}/merge.queue/*.ticket"
|
|
102
|
+
process = "merge.py"
|
|
103
|
+
owner_flag = "--owner"
|
|
104
|
+
history = "{git_dir}/merge-history.jsonl"
|
|
105
|
+
history_columns = [{ key = "fields.duration_seconds", label = "Took", format = "seconds" }]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
| Adapter | Watches | Options |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `github-actions` | Workflow runs, with jobs and steps in the inspector | `repo` |
|
|
111
|
+
| `github-merge-queue` | GitHub's native merge queue: position, state, author and GitHub's merge estimate per pull request | `repo`, `branch` |
|
|
112
|
+
| `command` | Anything else: a program that prints one snapshot | `command`, `host`, `cwd`, `timeout` |
|
|
113
|
+
| `flock-queue` | A Linux FIFO of flock-held ticket files guarding one merge lock, plus an optional JSON Lines attempt log | `lock`, `tickets`, `process`, `root`, `host`, `interpreters`, `owner_flag`, `history`, `history_limit`, `history_columns` |
|
|
114
|
+
|
|
115
|
+
Merge queue states map as `QUEUED` and `MERGEABLE` to queued, `AWAITING_CHECKS` and `LOCKED` to running, and `UNMERGEABLE` to blocked. A branch without a queue shows a warning instead of an error.
|
|
116
|
+
|
|
117
|
+
`flock-queue` reads lock holders from `/proc/locks` rather than acquiring them. With `host` set, the probe goes to `python3 -` over SSH on stdin and nothing is installed on the host.
|
|
118
|
+
|
|
119
|
+
### Command sources
|
|
120
|
+
|
|
121
|
+
Use the `command` adapter for GitLab, Buildkite, Jenkins, Bors or an in-house queue. `command` is an argument list run without a shell. Locally it runs in `cwd`, which defaults to the config file's directory; relative paths resolve against that directory too. With `host` set, it runs over SSH (`BatchMode`, every argument quoted for the remote shell), in `cwd` on the host when given.
|
|
122
|
+
|
|
123
|
+
The program must exit zero and print one snapshot (below) on stdout within `timeout` seconds. Anything else, including a snapshot that breaks the contract, shows as a source error while the last good data stays on screen. [`examples/`](https://github.com/wolfiesch/queue-watch/tree/main/examples) has a working script and config.
|
|
124
|
+
|
|
125
|
+
## Snapshot contract
|
|
126
|
+
|
|
127
|
+
Every adapter emits the same versioned snapshot (`queue-watch.snapshot/1`): a list of records with `id`, `kind`, `state` and `title`, plus optional `ref`, `sha`, `owner`, `position`, timestamps, `url`, `fields` and `children`. `state` is one of `queued`, `running`, `blocked`, `success`, `failure`, `cancelled`, `neutral` or `unknown`; the adapter's own term is kept in `raw_state`. Records that share a `ref` or `sha` describe the same change across sources. [`model.py`](https://github.com/wolfiesch/queue-watch/blob/main/src/queue_watch/model.py) defines and validates the contract, and `--once` prints it.
|
|
128
|
+
|
|
129
|
+
## Development
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
uv sync
|
|
133
|
+
uv run pytest -q # includes SVG snapshot tests
|
|
134
|
+
uv run pytest -q --snapshot-update # after an intended layout change
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`docs/screenshot.png` is the wide layout snapshot rendered in a browser. To release, bump `__version__` in `src/queue_watch/__init__.py`, merge, and push a matching `vX.Y.Z` tag; the release workflow tests, builds, publishes to PyPI with trusted publishing, and creates the GitHub release.
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# queue-watch
|
|
2
|
+
|
|
3
|
+
Read-only terminal dashboard for CI runs and merge queues. It joins records from every source into one row per change, so a branch's CI runs and its merge-queue entry read together, and draws each merge queue as a chain of chips in queue order. queue-watch never reruns, cancels, merges or takes a lock.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
uv tool install queue-watch # or: pipx install queue-watch
|
|
11
|
+
uvx queue-watch # run once without installing
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Requires Python 3.11+. The GitHub adapters use an authenticated [GitHub CLI](https://cli.github.com/) (`gh auth login`).
|
|
15
|
+
|
|
16
|
+
## Use
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
queue-watch # inside a GitHub checkout
|
|
20
|
+
queue-watch --repo owner/repo # Actions plus the merge queue, if any
|
|
21
|
+
queue-watch --profile work # ~/.config/queue-watch/work.toml
|
|
22
|
+
queue-watch --config sources.toml # any combination of sources
|
|
23
|
+
queue-watch --repo owner/repo --once # one JSON snapshot, no TUI
|
|
24
|
+
queue-watch --theme nord --ascii # any Textual theme; ASCII glyphs only
|
|
25
|
+
queue-watch --density comfortable # title, owner and PR on a second line
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
With no flags, queue-watch looks for a `.queue-watch.toml` at the root of the current Git checkout. Without one, it watches the checkout's GitHub repository: Actions runs, plus the merge queue when the default branch has one. `--profile NAME` reads `$XDG_CONFIG_HOME/queue-watch/NAME.toml` (default `~/.config`).
|
|
29
|
+
|
|
30
|
+
The Changes tab groups changes into Needs attention, Running, Queued and Done; every view a source reports also gets its own tab. Done starts folded while any other group has changes; press enter on a group row, or click it, to fold or unfold it. Select a change or a queue chip to see its CI checks, the jobs of the most notable one, its queue position and ETA, and recent history in the inspector.
|
|
31
|
+
|
|
32
|
+
Each merge queue gets a strip above the tabs: its entries in merge order, when it should clear, merges in the last hour, and how long recent attempts took. Queue depth over the session appears as a sparkline once two fetches have answered; sources report only the current queue, so the history starts when queue-watch does. On terminals under 30 rows the entries shrink to one line each.
|
|
33
|
+
|
|
34
|
+
Keyboard: arrows to move, `1` to `9` for tabs, `/` to search (esc to clear), `o` to open the selection in a browser, `c` to copy its SHA, `d` to switch row density, `r` to refresh, `p` to pause, `q` to quit. The mouse works too: click tabs, rows, chips and the inspector buttons, and scroll with the wheel. When a source fails, its last good data stays on screen and its status pill turns stale.
|
|
35
|
+
|
|
36
|
+
### Repository configs need trust
|
|
37
|
+
|
|
38
|
+
A config can run commands and open SSH connections, so a `.queue-watch.toml` that arrives with a clone does nothing until you review it and run:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
queue-watch trust
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Trust covers the file's exact bytes (SHA-256, stored in `$XDG_STATE_HOME/queue-watch/trusted.json`). After any edit, queue-watch refuses the file and names the command to run again. Files passed with `--config` or `--profile` are yours and load without this step.
|
|
45
|
+
|
|
46
|
+
## Sources
|
|
47
|
+
|
|
48
|
+
A config file lists sources as `[[source]]` tables. `id` and `adapter` are required; `label` and `interval` (seconds) are optional; every other key is an adapter option.
|
|
49
|
+
|
|
50
|
+
```toml
|
|
51
|
+
[[source]]
|
|
52
|
+
id = "ci"
|
|
53
|
+
adapter = "github-actions"
|
|
54
|
+
repo = "owner/repo"
|
|
55
|
+
|
|
56
|
+
[[source]]
|
|
57
|
+
id = "merge-queue"
|
|
58
|
+
adapter = "github-merge-queue"
|
|
59
|
+
repo = "owner/repo"
|
|
60
|
+
branch = "main" # optional; default branch otherwise
|
|
61
|
+
|
|
62
|
+
[[source]]
|
|
63
|
+
id = "deploys"
|
|
64
|
+
adapter = "command"
|
|
65
|
+
command = ["./bin/deploy-status", "--json"]
|
|
66
|
+
timeout = 20 # optional seconds, default 25
|
|
67
|
+
|
|
68
|
+
[[source]]
|
|
69
|
+
id = "merges"
|
|
70
|
+
adapter = "flock-queue"
|
|
71
|
+
host = "build-host" # optional SSH destination
|
|
72
|
+
root = "~/src/project" # Git checkout that defines {git_dir}
|
|
73
|
+
lock = "{git_dir}/merge.lock"
|
|
74
|
+
tickets = "{git_dir}/merge.queue/*.ticket"
|
|
75
|
+
process = "merge.py"
|
|
76
|
+
owner_flag = "--owner"
|
|
77
|
+
history = "{git_dir}/merge-history.jsonl"
|
|
78
|
+
history_columns = [{ key = "fields.duration_seconds", label = "Took", format = "seconds" }]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
| Adapter | Watches | Options |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `github-actions` | Workflow runs, with jobs and steps in the inspector | `repo` |
|
|
84
|
+
| `github-merge-queue` | GitHub's native merge queue: position, state, author and GitHub's merge estimate per pull request | `repo`, `branch` |
|
|
85
|
+
| `command` | Anything else: a program that prints one snapshot | `command`, `host`, `cwd`, `timeout` |
|
|
86
|
+
| `flock-queue` | A Linux FIFO of flock-held ticket files guarding one merge lock, plus an optional JSON Lines attempt log | `lock`, `tickets`, `process`, `root`, `host`, `interpreters`, `owner_flag`, `history`, `history_limit`, `history_columns` |
|
|
87
|
+
|
|
88
|
+
Merge queue states map as `QUEUED` and `MERGEABLE` to queued, `AWAITING_CHECKS` and `LOCKED` to running, and `UNMERGEABLE` to blocked. A branch without a queue shows a warning instead of an error.
|
|
89
|
+
|
|
90
|
+
`flock-queue` reads lock holders from `/proc/locks` rather than acquiring them. With `host` set, the probe goes to `python3 -` over SSH on stdin and nothing is installed on the host.
|
|
91
|
+
|
|
92
|
+
### Command sources
|
|
93
|
+
|
|
94
|
+
Use the `command` adapter for GitLab, Buildkite, Jenkins, Bors or an in-house queue. `command` is an argument list run without a shell. Locally it runs in `cwd`, which defaults to the config file's directory; relative paths resolve against that directory too. With `host` set, it runs over SSH (`BatchMode`, every argument quoted for the remote shell), in `cwd` on the host when given.
|
|
95
|
+
|
|
96
|
+
The program must exit zero and print one snapshot (below) on stdout within `timeout` seconds. Anything else, including a snapshot that breaks the contract, shows as a source error while the last good data stays on screen. [`examples/`](https://github.com/wolfiesch/queue-watch/tree/main/examples) has a working script and config.
|
|
97
|
+
|
|
98
|
+
## Snapshot contract
|
|
99
|
+
|
|
100
|
+
Every adapter emits the same versioned snapshot (`queue-watch.snapshot/1`): a list of records with `id`, `kind`, `state` and `title`, plus optional `ref`, `sha`, `owner`, `position`, timestamps, `url`, `fields` and `children`. `state` is one of `queued`, `running`, `blocked`, `success`, `failure`, `cancelled`, `neutral` or `unknown`; the adapter's own term is kept in `raw_state`. Records that share a `ref` or `sha` describe the same change across sources. [`model.py`](https://github.com/wolfiesch/queue-watch/blob/main/src/queue_watch/model.py) defines and validates the contract, and `--once` prints it.
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
uv sync
|
|
106
|
+
uv run pytest -q # includes SVG snapshot tests
|
|
107
|
+
uv run pytest -q --snapshot-update # after an intended layout change
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`docs/screenshot.png` is the wide layout snapshot rendered in a browser. To release, bump `__version__` in `src/queue_watch/__init__.py`, merge, and push a matching `vX.Y.Z` tag; the release workflow tests, builds, publishes to PyPI with trusted publishing, and creates the GitHub release.
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Example source for the ``command`` adapter: a queue kept as a JSON file.
|
|
3
|
+
|
|
4
|
+
queue-watch runs this with no shell and reads one snapshot v1 object from
|
|
5
|
+
stdout. Adapt the ``load`` function to whatever your system exposes: an HTTP
|
|
6
|
+
API, a database query, a directory of ticket files.
|
|
7
|
+
|
|
8
|
+
[[source]]
|
|
9
|
+
id = "deploys"
|
|
10
|
+
adapter = "command"
|
|
11
|
+
command = ["python3", "examples/queue-status.py", "examples/queue.json"]
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from datetime import UTC, datetime
|
|
17
|
+
|
|
18
|
+
STATES = {"waiting": "queued", "deploying": "running", "held": "blocked"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load(path: str) -> list[dict]:
|
|
22
|
+
with open(path) as handle:
|
|
23
|
+
return json.load(handle)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main() -> None:
|
|
27
|
+
entries = load(sys.argv[1])
|
|
28
|
+
records = [
|
|
29
|
+
{
|
|
30
|
+
"id": str(entry["id"]),
|
|
31
|
+
"kind": "queue_entry",
|
|
32
|
+
"state": STATES.get(entry["status"], "unknown"),
|
|
33
|
+
"raw_state": entry["status"],
|
|
34
|
+
"title": entry["title"],
|
|
35
|
+
"owner": entry.get("owner"),
|
|
36
|
+
"position": position,
|
|
37
|
+
"created_at": entry.get("since"),
|
|
38
|
+
"fields": {"target": entry.get("target")},
|
|
39
|
+
}
|
|
40
|
+
for position, entry in enumerate(entries, start=1)
|
|
41
|
+
]
|
|
42
|
+
snapshot = {
|
|
43
|
+
"schema": "queue-watch.snapshot/1",
|
|
44
|
+
"observed_at": datetime.now(UTC).isoformat(),
|
|
45
|
+
"records": records,
|
|
46
|
+
"views": [
|
|
47
|
+
{
|
|
48
|
+
"kind": "queue_entry",
|
|
49
|
+
"label": "Deploys",
|
|
50
|
+
"columns": [
|
|
51
|
+
{"key": "position", "label": "#", "width": 4},
|
|
52
|
+
{"key": "state", "label": "State", "width": 12, "format": "state"},
|
|
53
|
+
{"key": "title", "label": "Change", "width": 36},
|
|
54
|
+
{"key": "fields.target", "label": "Target", "width": 12},
|
|
55
|
+
{"key": "created_at", "label": "Waiting", "width": 9, "format": "age"},
|
|
56
|
+
],
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
}
|
|
60
|
+
json.dump(snapshot, sys.stdout)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
main()
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
[
|
|
2
|
+
{"id": 41, "status": "deploying", "title": "Rotate API gateway certificates", "owner": "dana", "since": "2026-10-20T16:02:00+00:00", "target": "prod-eu"},
|
|
3
|
+
{"id": 42, "status": "waiting", "title": "Raise worker pool to 24", "owner": "lee", "since": "2026-10-20T16:10:00+00:00", "target": "prod-us"},
|
|
4
|
+
{"id": 43, "status": "held", "title": "Schema change: orders.index", "owner": "sam", "since": "2026-10-20T16:12:00+00:00", "target": "prod-us"}
|
|
5
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "queue-watch"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Read-only terminal dashboard for CI runs and merge queues, with mouse support"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "Wolfgang Schoenberger" }]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = ["textual>=6,<7"]
|
|
11
|
+
keywords = ["ci", "merge-queue", "github-actions", "tui", "textual", "dashboard"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Environment :: Console",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Operating System :: MacOS",
|
|
17
|
+
"Operating System :: POSIX :: Linux",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Software Development :: Build Tools",
|
|
24
|
+
"Topic :: Software Development :: Testing",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/wolfiesch/queue-watch"
|
|
29
|
+
Issues = "https://github.com/wolfiesch/queue-watch/issues"
|
|
30
|
+
Changelog = "https://github.com/wolfiesch/queue-watch/releases"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
queue-watch = "queue_watch.cli:main"
|
|
34
|
+
|
|
35
|
+
[dependency-groups]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=8",
|
|
38
|
+
"pytest-textual-snapshot>=1.0.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["hatchling>=1.27"]
|
|
43
|
+
build-backend = "hatchling.build"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.version]
|
|
46
|
+
path = "src/queue_watch/__init__.py"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.sdist]
|
|
49
|
+
include = ["/src", "/tests", "/examples", "/README.md", "/LICENSE"]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
line-length = 100
|
|
56
|
+
|
|
57
|
+
[tool.ruff.lint]
|
|
58
|
+
extend-select = ["I", "B", "UP"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Adapters turn one external system into snapshot v1 (see queue_watch.model)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Adapter:
|
|
9
|
+
"""Base class. Subclasses take their options as keyword-only arguments.
|
|
10
|
+
|
|
11
|
+
A ``config_dir`` parameter, when present, receives the directory of the config
|
|
12
|
+
file; it is supplied by the loader and cannot be set from configuration.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
name: ClassVar[str]
|
|
16
|
+
label: ClassVar[str]
|
|
17
|
+
interval: ClassVar[float] = 15.0
|
|
18
|
+
|
|
19
|
+
async def snapshot(self) -> dict:
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
|
|
22
|
+
async def children(self, record: dict) -> list[dict]:
|
|
23
|
+
"""Detail records (jobs, steps) for the inspector; inline by default."""
|
|
24
|
+
return record.get("children") or []
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def registry() -> dict[str, type[Adapter]]:
|
|
28
|
+
from .command import Command
|
|
29
|
+
from .flock_queue import FlockQueue
|
|
30
|
+
from .github_actions import GitHubActions
|
|
31
|
+
from .github_merge_queue import GitHubMergeQueue
|
|
32
|
+
|
|
33
|
+
adapters = (GitHubActions, GitHubMergeQueue, FlockQueue, Command)
|
|
34
|
+
return {adapter.name: adapter for adapter in adapters}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Any program that prints one snapshot v1 JSON object on stdout.
|
|
2
|
+
|
|
3
|
+
This is the extension point for systems without a built-in adapter (GitLab,
|
|
4
|
+
Buildkite, Jenkins, Bors, in-house queues). ``command`` is an argument list run
|
|
5
|
+
without a shell. Locally it runs in ``cwd``, which defaults to the directory of
|
|
6
|
+
the config file. With ``host`` set it runs over SSH, in ``cwd`` on that host
|
|
7
|
+
when given. A nonzero exit, a timeout or output that breaks the contract is
|
|
8
|
+
shown as a source error, and the last good data stays on screen.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from ..process import check_host, over_ssh, run_json
|
|
17
|
+
from . import Adapter
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Command(Adapter):
|
|
21
|
+
name = "command"
|
|
22
|
+
label = "Command"
|
|
23
|
+
interval = 15.0
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
command: list[str],
|
|
29
|
+
host: str | None = None,
|
|
30
|
+
cwd: str | None = None,
|
|
31
|
+
timeout: float = 25,
|
|
32
|
+
config_dir: Path | None = None,
|
|
33
|
+
):
|
|
34
|
+
if (
|
|
35
|
+
not isinstance(command, list)
|
|
36
|
+
or not command
|
|
37
|
+
or not all(isinstance(arg, str) and arg for arg in command)
|
|
38
|
+
):
|
|
39
|
+
raise ValueError(
|
|
40
|
+
'command must be a list of arguments, for example ["./status", "--json"]'
|
|
41
|
+
)
|
|
42
|
+
if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or timeout <= 0:
|
|
43
|
+
raise ValueError("timeout must be a positive number of seconds")
|
|
44
|
+
self.command, self.host, self.timeout = command, check_host(host), float(timeout)
|
|
45
|
+
if self.host is not None:
|
|
46
|
+
self.cwd = cwd
|
|
47
|
+
else:
|
|
48
|
+
base = config_dir or Path.cwd()
|
|
49
|
+
self.cwd = str(base / os.path.expanduser(cwd)) if cwd else str(base)
|
|
50
|
+
|
|
51
|
+
async def snapshot(self) -> dict:
|
|
52
|
+
if self.host is not None:
|
|
53
|
+
return await run_json(over_ssh(self.host, self.command, self.cwd), timeout=self.timeout)
|
|
54
|
+
return await run_json(self.command, cwd=self.cwd, timeout=self.timeout)
|