repoactive 0.3.1.dev3__tar.gz → 0.4.0.dev1__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.
Files changed (30) hide show
  1. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/PKG-INFO +345 -178
  2. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/README.md +341 -175
  3. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/pyproject.toml +4 -3
  4. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/src/repoactive/boxquote.py +14 -15
  5. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/src/repoactive/cli.py +13 -11
  6. repoactive-0.4.0.dev1/src/repoactive/command.py +171 -0
  7. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/src/repoactive/config.py +172 -56
  8. repoactive-0.4.0.dev1/src/repoactive/constants.py +43 -0
  9. repoactive-0.4.0.dev1/src/repoactive/generator.py +171 -0
  10. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/src/repoactive/graph.py +3 -3
  11. repoactive-0.4.0.dev1/src/repoactive/human_commits.py +114 -0
  12. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/src/repoactive/jj.py +235 -70
  13. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/src/repoactive/jobtree.py +9 -9
  14. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/src/repoactive/lock.py +6 -6
  15. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/src/repoactive/platforms/base.py +9 -0
  16. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/src/repoactive/platforms/github.py +17 -0
  17. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/src/repoactive/platforms/gitlab.py +21 -5
  18. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/src/repoactive/progress.py +14 -14
  19. repoactive-0.4.0.dev1/src/repoactive/runner.py +1298 -0
  20. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/src/repoactive/selection.py +30 -23
  21. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/src/repoactive/settings.py +7 -7
  22. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/src/repoactive/trailers.py +7 -7
  23. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/src/repoactive/ui.py +11 -11
  24. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/src/repoactive/updates.py +32 -9
  25. repoactive-0.3.1.dev3/src/repoactive/constants.py +0 -10
  26. repoactive-0.3.1.dev3/src/repoactive/runner.py +0 -1392
  27. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/.gitignore +0 -0
  28. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/LICENSE +0 -0
  29. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/src/repoactive/__init__.py +0 -0
  30. {repoactive-0.3.1.dev3 → repoactive-0.4.0.dev1}/src/repoactive/platforms/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: repoactive
3
- Version: 0.3.1.dev3
3
+ Version: 0.4.0.dev1
4
4
  Summary: Script-driven code changes with automated merge requests
5
5
  License-Expression: GPL-3.0-or-later
6
6
  License-File: LICENSE
@@ -8,15 +8,16 @@ Classifier: Development Status :: 3 - Alpha
8
8
  Classifier: Environment :: Console
9
9
  Classifier: Intended Audience :: Developers
10
10
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
11
+ Classifier: Operating System :: MacOS :: MacOS X
12
+ Classifier: Operating System :: POSIX
11
13
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.11
13
14
  Classifier: Programming Language :: Python :: 3.12
14
15
  Classifier: Programming Language :: Python :: 3.13
15
16
  Classifier: Programming Language :: Python :: 3.14
16
17
  Classifier: Topic :: Software Development
17
18
  Classifier: Topic :: Software Development :: Version Control
18
19
  Classifier: Topic :: Software Development :: Version Control :: Git
19
- Requires-Python: >=3.11
20
+ Requires-Python: >=3.12
20
21
  Requires-Dist: pydantic-settings>=2.14.2
21
22
  Requires-Dist: pydantic<3.0.0,>=2.13.4
22
23
  Requires-Dist: pygithub<3.0.0,>=2.9.1
@@ -27,28 +28,40 @@ Description-Content-Type: text/markdown
27
28
 
28
29
  # repoactive - Script-driven code changes with automated merge requests
29
30
 
31
+ [![PyPI](https://img.shields.io/pypi/v/repoactive.svg)](https://pypi.org/project/repoactive/)
32
+ [![CI](https://github.com/schmir/repoactive/actions/workflows/ci.yml/badge.svg)](https://github.com/schmir/repoactive/actions/workflows/ci.yml)
33
+ [![Python versions](https://img.shields.io/pypi/pyversions/repoactive.svg)](https://pypi.org/project/repoactive/)
34
+ [![License](https://img.shields.io/pypi/l/repoactive.svg)](LICENSE)
35
+
30
36
  > **Warning:** This project is in an early stage of development. Use at your
31
37
  > own risk.
32
38
 
33
39
  `repoactive` runs your scripts against a git repository and optionally keeps
34
40
  the corresponding merge requests up to date. You write the scripts that
35
41
  produce the code changes; `repoactive` handles the rest - branches, commits,
36
- and (with `--mode publish`) the full MR lifecycle.
42
+ and (with `--mode publish`) the full MR lifecycle. This document says merge
43
+ request (MR) throughout; on GitHub, read that as pull request (PR).
37
44
 
38
45
  ## Contents
39
46
 
40
47
  - [Use cases](#use-cases)
48
+ - [How it compares](#how-it-compares)
41
49
  - [Installation](#installation)
42
50
  - [Quick start](#quick-start)
43
51
  - [How it works](#how-it-works)
52
+ - [What a run produces](#what-a-run-produces)
44
53
  - [Keeping the local clone current](#keeping-the-local-clone-current)
45
- - [Usage](#usage)
54
+ - [Undoing a run](#undoing-a-run)
55
+ - [Commands](#commands)
56
+ - [`repoactive run`](#repoactive-run)
57
+ - [`repoactive recent-commits`](#repoactive-recent-commits)
58
+ - [`repoactive validate-config`](#repoactive-validate-config)
59
+ - [`repoactive dump-schema`](#repoactive-dump-schema)
60
+ - [`repoactive info jobs`](#repoactive-info-jobs)
61
+ - [`repoactive info tags`](#repoactive-info-tags)
46
62
  - [Environment variables](#environment-variables)
47
- - [Inspecting repoactive commits](#inspecting-repoactive-commits)
48
- - [Validating configuration](#validating-configuration)
49
- - [Listing jobs](#listing-jobs)
50
- - [Listing tags](#listing-tags)
51
63
  - [Configuration](#configuration)
64
+ - [Editor support](#editor-support)
52
65
  - [`[job-defaults]`](#job-defaults)
53
66
  - [`[job.<name>]`](#jobname)
54
67
  - [Stacking MRs](#stacking-mrs)
@@ -58,19 +71,20 @@ and (with `--mode publish`) the full MR lifecycle.
58
71
  - [Variables passed to job commands](#variables-passed-to-job-commands)
59
72
  - [Passing secrets to commands with `secret_env`](#passing-secrets-to-commands-with-secret_env)
60
73
  - [Overriding values on the command line](#overriding-values-on-the-command-line)
61
- - [Selecting jobs with tags](#selecting-jobs-with-tags)
62
- - [Keeping unmerged branches current](#keeping-unmerged-branches-current)
63
- - [Disabling jobs](#disabling-jobs)
64
- - [Running a job on a schedule](#running-a-job-on-a-schedule)
65
- - [One run at a time per repository](#one-run-at-a-time-per-repository)
66
- - [Gating jobs with `run_only_if_changed`](#gating-jobs-with-run_only_if_changed)
67
- - [Throttling jobs with `cooldown_period`](#throttling-jobs-with-cooldown_period)
68
- - [Throttling a job when a superset lands with `cooldown_on`](#throttling-a-job-when-a-superset-lands-with-cooldown_on)
69
- - [Limiting job runtime with `timeout`](#limiting-job-runtime-with-timeout)
70
- - [Generating jobs dynamically](#generating-jobs-dynamically)
74
+ - [Run control](#run-control)
75
+ - [Selecting jobs with tags](#selecting-jobs-with-tags)
76
+ - [Disabling jobs](#disabling-jobs)
77
+ - [Running a job on a schedule](#running-a-job-on-a-schedule)
78
+ - [Gating jobs with `run_only_if_changed`](#gating-jobs-with-run_only_if_changed)
79
+ - [Throttling jobs with `cooldown_period`](#throttling-jobs-with-cooldown_period)
80
+ - [Throttling a job when a superset lands with `cooldown_on`](#throttling-a-job-when-a-superset-lands-with-cooldown_on)
81
+ - [Limiting job runtime with `timeout`](#limiting-job-runtime-with-timeout)
82
+ - [Generating jobs dynamically](#generating-jobs-dynamically)
71
83
  - [Requirements](#requirements)
72
84
  - [Appendix](#appendix)
73
85
  - [jj revset aliases](#jj-revset-aliases)
86
+ - [Changelog](#changelog)
87
+ - [License](#license)
74
88
 
75
89
  ## Use cases
76
90
 
@@ -81,6 +95,23 @@ and (with `--mode publish`) the full MR lifecycle.
81
95
  - Automating any periodic code transformation that should go through a
82
96
  review process
83
97
 
98
+ ### How it compares
99
+
100
+ [Renovate](https://docs.renovatebot.com/) and
101
+ [Dependabot](https://docs.github.com/en/code-security/dependabot) are built
102
+ around dependency updates; repoactive runs whatever script you write, so the
103
+ change can be anything the repository generates - a lock-file upgrade is
104
+ just the simplest case.
105
+ [multi-gitter](https://github.com/lindell/multi-gitter) also runs your
106
+ script and opens the MR, but as a one-shot bulk operation across
107
+ repositories; repoactive is built for re-running against one repository: an
108
+ unchanged diff leaves the MR untouched, a changed one updates it, branches
109
+ are rebased onto the moving base branch, cooldowns keep a landed change from
110
+ immediately reopening, and jobs can stack their MRs on one another. And it
111
+ is a plain CLI working against your local clone - no hosted service and
112
+ nothing to install into the repository - that talks to GitHub and GitLab,
113
+ including self-hosted instances.
114
+
84
115
  ## Installation
85
116
 
86
117
  `repoactive` is published on [PyPI](https://pypi.org/project/repoactive/).
@@ -125,13 +156,25 @@ needs a GitHub or GitLab API token in the environment. See
125
156
  4. **Run the job locally.** In the default `local` mode nothing is pushed
126
157
  and no MR is created - repoactive just records the diff your script
127
158
  produced on the branch `repoactive/uv-lock-upgrade` and prints a
128
- `jj op restore` command to undo the run:
159
+ `jj op restore` command to [undo the run](#undoing-a-run):
129
160
 
130
161
  ```bash
131
162
  repoactive run
132
163
  ```
133
164
 
134
- 5. **Publish it.** Put the API token your platform uses in the environment
165
+ 5. **Look at what it produced.** The change lives on the job's branch, so
166
+ your own working copy still looks untouched:
167
+
168
+ ```bash
169
+ repoactive recent-commits # every repoactive commit, newest first
170
+ jj diff -r repoactive/uv-lock-upgrade # the diff the script produced
171
+ git show repoactive/uv-lock-upgrade # the same, through colocated git
172
+ ```
173
+
174
+ The appendix has [jj revset aliases](#jj-revset-aliases) for querying
175
+ repoactive's commits directly.
176
+
177
+ 6. **Publish it.** Put the API token your platform uses in the environment
135
178
  (`GITHUB_TOKEN` for GitHub.com, `GITLAB_TOKEN` for GitLab.com by
136
179
  default), fetch the latest base branch, then let repoactive push the
137
180
  branch and open - or update - the merge request:
@@ -148,9 +191,9 @@ needs a GitHub or GitLab API token in the environment. See
148
191
  ## How it works
149
192
 
150
193
  You configure one or more **jobs**, each with a script (any shell command or
151
- executable) that modifies the repository's working tree. `repoactive` runs
152
- each script, captures the resulting diff, and records the change locally.
153
- With `--mode publish` it also:
194
+ executable) that modifies the repository's files. `repoactive` runs each
195
+ script, captures the resulting diff, and records the change locally. With
196
+ `--mode publish` it also:
154
197
 
155
198
  - opens a new merge request if one does not already exist for that job, or
156
199
  - updates the existing merge request branch if the diff has changed.
@@ -158,17 +201,18 @@ With `--mode publish` it also:
158
201
  Branches and MR descriptions are managed automatically - the only code you
159
202
  need to write is the script that produces the change.
160
203
 
161
- ```
162
- [your script] → diff → repoactive → branch
163
- ↓ (with --mode push or --mode publish)
164
- git push → merge request
165
- ↑ (with --mode publish)
166
- (create or update)
167
- ```
168
-
169
204
  1. `repoactive` creates a new commit on top of the base branch or on top of
170
- other repoactive managed branches.
171
- 2. It runs the job's script against the working tree.
205
+ other repoactive managed branches, and checks that commit out into a
206
+ temporary jj workspace.
207
+ 2. It runs the job's script in that workspace, which is also the script's
208
+ working directory. **Your own working copy is never touched** - a run is
209
+ safe with uncommitted changes in your checkout, and the script sees a
210
+ clean tree at the base branch rather than whatever you have lying around.
211
+ The workspace is a full checkout, so paths relative to the repository
212
+ root resolve normally; [`RA_WORKSPACE_DIR`](#ra_workspace_dir) names it
213
+ for scripts that change directory, and
214
+ [`RA_CONFIG_SOURCE_DIR`](#ra_config_source_dir) reaches helper files kept
215
+ beside the config. The workspace is discarded when the job finishes.
172
216
  3. If the script produced a diff, it records the change. With `--mode push`
173
217
  or `--mode publish`, it pushes the branch; with `--mode publish`, it also
174
218
  creates or updates the merge request. On a re-run, if the diff matches
@@ -183,20 +227,63 @@ need to write is the script that produces the change.
183
227
  is now stale, so it is deleted; with `--mode push` or `--mode publish`,
184
228
  the deletion is pushed to the remote.
185
229
 
186
- > **jj commits the whole working tree.** Because `repoactive` uses jj, every
187
- > new file your script creates inside the working directory is added to the
188
- > commit unless it is git-ignored. There is no way to select which
189
- > working-tree changes become part of the commit - they all will. Keep
190
- > `.gitignore` up to date so build artifacts, caches, and other stray files
191
- > your script produces do not end up in the diff.
230
+ > **jj commits the whole workspace.** Because `repoactive` uses jj, every
231
+ > new file your script creates inside the workspace is added to the commit
232
+ > unless it is git-ignored. There is no way to select which of the script's
233
+ > changes become part of the commit - they all will. Keep `.gitignore` up to
234
+ > date so build artifacts, caches, and other stray files your script
235
+ > produces do not end up in the diff.
236
+
237
+ ### What a run produces
238
+
239
+ For the quick-start job above, a `--mode publish` run pushes the branch
240
+ `repoactive/uv-lock-upgrade` and opens an MR titled
241
+ `[repoactive] build: upgrade dependencies`. Its description holds the job's
242
+ `description` (when set), the command and its captured output in a code
243
+ block, and - for [stacked jobs](#stacking-mrs) - links to the parent MRs
244
+ ([a real example](https://github.com/schmir/repoactive/pull/7)):
245
+
246
+ ```
247
+ $ uv lock --upgrade
248
+ Using CPython 3.14.1
249
+ Resolved 51 packages in 4.19s
250
+ Updated annotated-types v0.7.0 -> v0.8.0
251
+ Updated nox v2026.4.10 -> v2026.7.11
252
+ Updated ruff v0.15.21 -> v0.16.0
253
+ Updated ty v0.0.58 -> v0.0.63
254
+ ```
255
+
256
+ The commit carries the same output (unless `output_in_commit` is disabled)
257
+ plus the `Repoactive-Job` trailer that
258
+ [`recent-commits`](#repoactive-recent-commits), the
259
+ [cooldown check](#throttling-jobs-with-cooldown_period), and the
260
+ [unmerged-branch refresh](#keeping-unmerged-branches-current) all key on:
261
+
262
+ ```
263
+ [repoactive] build: upgrade dependencies
264
+
265
+ ,----[ uv lock --upgrade ]
266
+ | Using CPython 3.14.1
267
+ | Resolved 51 packages in 4.19s
268
+ | Updated annotated-types v0.7.0 -> v0.8.0
269
+ | Updated nox v2026.4.10 -> v2026.7.11
270
+ | Updated ruff v0.15.21 -> v0.16.0
271
+ | Updated ty v0.0.58 -> v0.0.63
272
+ `----
273
+
274
+ Repoactive-Job: uv-lock-upgrade
275
+ ```
192
276
 
193
277
  ### Keeping the local clone current
194
278
 
195
279
  `repoactive` works entirely from the **local** repository view and never
196
- fetches from the remote on its own. Rebasing onto `trunk()`, cooldown
197
- throttling, and the unmerged-branch refresh all read the local `trunk()` /
198
- base branches, so a merge that happened on the remote is invisible until the
199
- local clone advances past it.
280
+ fetches from the remote on its own. Jobs target `trunk()` unless they set a
281
+ `base_branch` - that is jj's built-in revset for the tip of the repository's
282
+ main branch (`main@origin`, `master@origin`, and friends), as the local
283
+ clone last saw it. Rebasing onto `trunk()`, cooldown throttling, and the
284
+ unmerged-branch refresh all read the local `trunk()` / base branches, so a
285
+ merge that happened on the remote is invisible until the local clone
286
+ advances past it.
200
287
 
201
288
  **Fetch before each run.** Run `jj git fetch` (or `git fetch --prune`) in
202
289
  the same cron job or CI pipeline that invokes `repoactive`, before it. If
@@ -206,13 +293,42 @@ because the commit that would trigger it has not reached the local base
206
293
  branch. See
207
294
  [ADR 0005](docs/adr/0005-local-repository-is-the-source-of-truth.md).
208
295
 
209
- ## Usage
296
+ ### Undoing a run
297
+
298
+ Every `run` captures the jj operation id beforehand and prints a
299
+ `jj op restore <id>` command at the end of the run (last, since a run can
300
+ produce a lot of output). Run it to roll the local repository - commits,
301
+ bookmarks and colocated git refs - back to the state it was in before the
302
+ run. It only undoes local changes: a branch already pushed or an MR already
303
+ created by a `--mode push`/`--mode publish` run is not affected, as the hint
304
+ panel itself points out.
305
+
306
+ ## Commands
210
307
 
211
308
  ```bash
212
309
  # Print the installed version and exit
213
310
  repoactive --version
214
311
  ```
215
312
 
313
+ Every command that works against a repository accepts `--repo`/`-r` and
314
+ `--debug`/`-d`. The commands that read configuration - `run`,
315
+ `validate-config`, `info jobs`, and `info tags` - also accept the same
316
+ `--config`/`-c` and `--set`/`-s` options, described in the
317
+ [`repoactive run`](#repoactive-run) table below. `recent-commits` works from
318
+ the repository alone and reads no configuration;
319
+ [`dump-schema`](#repoactive-dump-schema) touches no repository at all and
320
+ takes only `--output`.
321
+
322
+ All commands exit with these status codes:
323
+
324
+ | Code | Meaning |
325
+ | ---- | ------------------------------------------------------------------------------------------ |
326
+ | 0 | Success |
327
+ | 1 | Failure (invalid configuration, a failed job, ...) |
328
+ | 2 | `run` only: another run holds the [per-repository lock](#one-run-at-a-time-per-repository) |
329
+
330
+ ### `repoactive run`
331
+
216
332
  ```
217
333
  repoactive run [OPTIONS] [JOBS]...
218
334
  ```
@@ -249,64 +365,7 @@ repoactive run --debug
249
365
  | `--tag TAG` | `-t` | Run jobs carrying any of these tags (repeatable). With no tags/jobs the default run targets the `enabled` tag |
250
366
  | `--debug` | `-d` | Enable debug logging |
251
367
 
252
- ### Environment variables
253
-
254
- Besides the command-line options, repoactive reads a few `REPOACTIVE_*`
255
- environment variables that tune how it presents itself in the current
256
- environment (as opposed to the [configuration](#configuration), which
257
- describes the jobs to run). They are handled uniformly: all of them go
258
- through a single settings model and are validated together at startup, so a
259
- misconfigured variable fails immediately with a one-line error naming it
260
- instead of surfacing mid-run or being silently ignored. Values are
261
- case-insensitive.
262
-
263
- #### `REPOACTIVE_UI`
264
-
265
- `interactive` (default) or `noninteractive`.
266
-
267
- Every `run` captures the jj operation id beforehand and prints a
268
- `jj op restore <id>` command at the end of the run (last, since a run can
269
- produce a lot of output). Run it to roll the local repository - commits,
270
- bookmarks and colocated git refs - back to the state it was in before the
271
- run. It only undoes local changes: a branch already pushed or an MR already
272
- created by a `--mode push`/`--mode publish` run is not affected, as the hint
273
- panel itself points out.
274
-
275
- `noninteractive` suppresses these "how to undo" hint panels. Set it where
276
- nobody is at the keyboard, say an unattended CI job. This is an explicit
277
- switch rather than CI auto-detection, because a CI container someone has
278
- logged in to _is_ interactive.
279
-
280
- #### `REPOACTIVE_LOG_LEVEL`
281
-
282
- `debug`, `info`, `warning`, `error`, or `critical`; unset by default
283
- (logging off).
284
-
285
- Enables logging at that level without passing `--debug` on every invocation;
286
- an explicit `--debug` takes precedence.
287
-
288
- #### `REPOACTIVE_LOG_HANDLER`
289
-
290
- `rich` or `plain`; defaults to `rich`, or `plain` when `REPOACTIVE_UI` is
291
- `noninteractive`.
292
-
293
- How log records are rendered: a colorized column layout via
294
- [rich](https://github.com/Textualize/rich), or the stdlib's plain stream
295
- handler (e.g. when the output is collected by a log aggregator).
296
-
297
- #### `REPOACTIVE_PROGRESS_LINES`
298
-
299
- An integer (default: `8`).
300
-
301
- While a job's command runs in an interactive terminal, repoactive shows a
302
- live, scrolling block of its most recent output lines. The block stays on
303
- screen once the command finishes, with the job's status line printed below
304
- it. `REPOACTIVE_PROGRESS_LINES` sets how many lines that live tail shows;
305
- `0` or less disables the live block entirely. When output is not a terminal
306
- (piped or in CI) the block is disabled and the command's output is left
307
- untouched.
308
-
309
- ## Inspecting repoactive commits
368
+ ### `repoactive recent-commits`
310
369
 
311
370
  ```
312
371
  repoactive recent-commits [OPTIONS] [JOBS]...
@@ -339,7 +398,7 @@ repoactive recent-commits --status unmerged
339
398
  | `--status [all\|merged\|unmerged]` | `-s` | Filter by merge status into trunk (default: `all`) |
340
399
  | `--debug` | `-d` | Enable debug logging |
341
400
 
342
- ## Validating configuration
401
+ ### `repoactive validate-config`
343
402
 
344
403
  ```
345
404
  repoactive validate-config [OPTIONS]
@@ -366,10 +425,29 @@ code 1.
366
425
  Validation checks include unknown keys, missing required fields, invalid
367
426
  `depends_on` references, and circular job dependencies.
368
427
 
369
- The command accepts the same `--config`, `--set`, `--repo`, and `--debug`
370
- options as [`repoactive run`](#usage).
428
+ ### `repoactive dump-schema`
429
+
430
+ ```
431
+ repoactive dump-schema --output PATH
432
+ ```
371
433
 
372
- ## Listing jobs
434
+ Write the JSON schema of the TOML config to a file. Use it to pin the schema
435
+ your editor validates against to the version you have installed, rather than
436
+ the published one:
437
+
438
+ ```bash
439
+ repoactive dump-schema -o config-schema.json
440
+ ```
441
+
442
+ See [Editor support](#editor-support) for wiring it up. Unlike the other
443
+ commands this one only reads the installed models, so it takes neither
444
+ `--repo` nor `--debug`.
445
+
446
+ | Option | Short | Description |
447
+ | --------------- | ----- | -------------------------------------- |
448
+ | `--output PATH` | `-o` | File to write the schema to (required) |
449
+
450
+ ### `repoactive info jobs`
373
451
 
374
452
  ```
375
453
  repoactive info jobs [OPTIONS]
@@ -393,10 +471,7 @@ build Build the project enabled
393
471
  └── deploy Deploy to staging nightly, risky
394
472
  ```
395
473
 
396
- The command accepts the same `--config`, `--set`, `--repo`, and `--debug`
397
- options as [`repoactive run`](#usage).
398
-
399
- ## Listing tags
474
+ ### `repoactive info tags`
400
475
 
401
476
  ```
402
477
  repoactive info tags [OPTIONS]
@@ -428,8 +503,54 @@ nightly:
428
503
  benchmark Run nightly benchmarks nightly
429
504
  ```
430
505
 
431
- The command accepts the same `--config`, `--set`, `--repo`, and `--debug`
432
- options as [`repoactive run`](#usage).
506
+ ### Environment variables
507
+
508
+ Besides the command-line options, repoactive reads a few `REPOACTIVE_*`
509
+ environment variables that tune how it presents itself in the current
510
+ environment (as opposed to the [configuration](#configuration), which
511
+ describes the jobs to run). They are handled uniformly: all of them go
512
+ through a single settings model and are validated together at startup, so a
513
+ misconfigured variable fails immediately with a one-line error naming it
514
+ instead of surfacing mid-run or being silently ignored. Values are
515
+ case-insensitive.
516
+
517
+ #### `REPOACTIVE_UI`
518
+
519
+ `interactive` (default) or `noninteractive`.
520
+
521
+ `noninteractive` suppresses the ["how to undo" hint panels](#undoing-a-run)
522
+ printed after each run. Set it where nobody is at the keyboard, say an
523
+ unattended CI job. This is an explicit switch rather than CI auto-detection,
524
+ because a CI container someone has logged in to _is_ interactive.
525
+
526
+ #### `REPOACTIVE_LOG_LEVEL`
527
+
528
+ `debug`, `info`, `warning`, `error`, or `critical`; unset by default
529
+ (logging off).
530
+
531
+ Enables logging at that level without passing `--debug` on every invocation;
532
+ an explicit `--debug` takes precedence.
533
+
534
+ #### `REPOACTIVE_LOG_HANDLER`
535
+
536
+ `rich` or `plain`; defaults to `rich`, or `plain` when `REPOACTIVE_UI` is
537
+ `noninteractive`.
538
+
539
+ How log records are rendered: a colorized column layout via
540
+ [rich](https://github.com/Textualize/rich), or the stdlib's plain stream
541
+ handler (e.g. when the output is collected by a log aggregator).
542
+
543
+ #### `REPOACTIVE_PROGRESS_LINES`
544
+
545
+ An integer (default: `8`).
546
+
547
+ While a job's command runs in an interactive terminal, repoactive shows a
548
+ live, scrolling block of its most recent output lines. The block stays on
549
+ screen once the command finishes, with the job's status line printed below
550
+ it. `REPOACTIVE_PROGRESS_LINES` sets how many lines that live tail shows;
551
+ `0` or less disables the live block entirely. When output is not a terminal
552
+ (piped or in CI) the block is disabled and the command's output is left
553
+ untouched.
433
554
 
434
555
  ## Configuration
435
556
 
@@ -450,16 +571,37 @@ Every key in `[job-defaults]` supplies the default for the matching per-job
450
571
  key; any job may override it by setting the same key in its `[job.<name>]`
451
572
  block.
452
573
 
574
+ ### Editor support
575
+
576
+ `repoactive` publishes a JSON schema for the config format, so an editor
577
+ with TOML schema support (Even Better TOML in VS Code, Taplo, the JetBrains
578
+ TOML plugin) can complete field names, show what each one does, and flag
579
+ typos as you type - useful given how many optional keys a job accepts. Point
580
+ the `"$schema"` key at the published schema:
581
+
582
+ ```toml
583
+ "$schema" = "https://raw.githubusercontent.com/schmir/repoactive/refs/heads/main/config-schema.json"
584
+
585
+ [job.uv-lock-upgrade]
586
+ command = "uv lock --upgrade"
587
+ title = "build: upgrade dependencies"
588
+ ```
589
+
590
+ `"$schema"` is part of the config format: `repoactive` accepts it and
591
+ ignores it. The URL above tracks `main`; to validate against the version you
592
+ actually have installed, write the schema out with
593
+ [`repoactive dump-schema`](#repoactive-dump-schema) and point `"$schema"` at
594
+ that file instead.
595
+
453
596
  ### `[job-defaults]`
454
597
 
455
- **MR/PR options:**
598
+ **MR options:**
456
599
 
457
- - **`labels`** (default: `[]`) - Labels applied to every MR/PR. Per-job
600
+ - **`labels`** (default: `[]`) - Labels applied to every MR. Per-job
458
601
  `labels` are merged with, not replaced by, this list.
459
602
  - **`auto_merge`** (default: `false`) - When `true`, enable auto-merge on
460
- every MR/PR so it merges automatically once its pipeline passes. On
461
- GitHub, the repository must have "Allow auto-merge" enabled in its
462
- settings.
603
+ every MR so it merges automatically once its pipeline passes. On GitHub,
604
+ the repository must have "Allow auto-merge" enabled in its settings.
463
605
  - **`required_approvals`** (default: none) - Minimum number of approvals
464
606
  required before the MR can be merged. **GitLab only** - this sets the
465
607
  per-MR approval requirement. GitHub has no per-PR equivalent (required
@@ -474,12 +616,13 @@ block.
474
616
  no job is named after your base branch (e.g. a job named `main`) - the two
475
617
  branches would otherwise collide.
476
618
  - **`mr_title_prefix`** (default: `"[repoactive] "`) - Prefix prepended to
477
- every MR/PR title. Set to `""` to disable.
619
+ every MR title. Set to `""` to disable.
478
620
  - **`commit_title_prefix`** (default: `"[repoactive] "`) - Prefix prepended
479
621
  to every commit title. Set to `""` to disable.
480
- - **`base_branch`** (default: repo default) - Target branch for all jobs
481
- that do not set their own. May also be a jj revset expression such as
482
- `trunk()`, `root()`, or a user-defined revset alias.
622
+ - **`base_branch`** (default: `trunk()`, the repository's main branch as the
623
+ local clone last saw it) - Target branch for all jobs that do not set
624
+ their own. May also be a jj revset expression such as `trunk()`, `root()`,
625
+ or a user-defined revset alias.
483
626
 
484
627
  **Run control:**
485
628
 
@@ -511,37 +654,38 @@ underscores.
511
654
 
512
655
  **Required:**
513
656
 
514
- - **`command`** - Shell command (or executable path) run in the repository
515
- working directory. A non-zero exit is a failure.
516
- - **`title`** - MR/PR title (also the commit subject, after
657
+ - **`command`** - Shell command (or executable path) run in the job's
658
+ temporary workspace, not in your checkout (see
659
+ [How it works](#how-it-works)). A non-zero exit is a failure.
660
+ - **`title`** - MR title (also the commit subject, after
517
661
  `commit_title_prefix`).
518
662
 
519
- **MR/PR options:**
663
+ **MR options:**
520
664
 
521
- - **`description`** - Body text of the MR/PR.
522
- - **`labels`** (default: `[]`) - Extra labels for this job's MR/PR, merged
523
- with `job-defaults.labels`.
524
- - **`draft`** (default: `false`) - Open the MR/PR as a draft. On GitHub,
525
- draft state cannot be changed after creation.
526
- - **`create_mr`** (default: `true`) - Whether to create an MR/PR: `true`
665
+ - **`description`** - Body text of the MR.
666
+ - **`labels`** (default: `[]`) - Extra labels for this job's MR, merged with
667
+ `job-defaults.labels`.
668
+ - **`draft`** (default: `false`) - Open the MR as a draft. On GitHub, draft
669
+ state cannot be changed after creation.
670
+ - **`create_mr`** (default: `true`) - Whether to create an MR: `true`
527
671
  (always), `false` (push the branch but skip the MR), or
528
672
  `"unless-superseded"` (skip when a dependent's MR from the same run
529
673
  already contains this job's changes - see [Stacking MRs](#stacking-mrs)).
530
674
  - **`auto_merge`** (default: inherited from `job-defaults`) - When `true`,
531
- enable auto-merge on this job's MR/PR.
675
+ enable auto-merge on this job's MR.
532
676
  - **`required_approvals`** (default: inherited from `job-defaults`) -
533
677
  Minimum number of approvals required before this job's MR can be merged.
534
678
  GitLab only; see `job-defaults.required_approvals`.
535
679
 
536
680
  **Branch and commit options:**
537
681
 
538
- - **`base_branch`** (default: inherited) - Target branch for this job's
539
- MR/PR. May also be a jj revset expression such as `trunk()`, `root()`, or
540
- a user-defined revset alias.
682
+ - **`base_branch`** (default: inherited, ultimately `trunk()`) - Target
683
+ branch for this job's MR. May also be a jj revset expression such as
684
+ `trunk()`, `root()`, or a user-defined revset alias.
541
685
  - **`branch_prefix`** (default: inherited) - Override the branch-name prefix
542
686
  for this job only.
543
- - **`mr_title_prefix`** (default: inherited) - Override the MR/PR title
544
- prefix for this job only.
687
+ - **`mr_title_prefix`** (default: inherited) - Override the MR title prefix
688
+ for this job only.
545
689
  - **`commit_title_prefix`** (default: inherited) - Override the commit title
546
690
  prefix for this job only.
547
691
  - **`output_in_commit`** (default: `true`) - Append the job's command and
@@ -599,14 +743,13 @@ Because a dependent's MR already contains its dependencies' changes, a
599
743
  dependency chain normally opens one MR per job that produced a diff. Setting
600
744
  `create_mr = "unless-superseded"` on the earlier jobs collapses that: such a
601
745
  job skips its MR when a dependent job produced an MR in the same run, so the
602
- chain yields a single MR on the topmost job that actually changed something
603
-
604
- - falling back to the job below it when the jobs above came up empty. The
605
- branch is still pushed either way. Only the current run counts: a
606
- dependent that is empty, failed, on cooldown, or not selected does not
607
- suppress anything. See
608
- [ADR 0009](docs/adr/0009-unless-superseded-mr-creation.md) for details and
609
- limitations.
746
+ chain yields a single MR on the topmost job that actually changed
747
+ something - falling back to the job below it when the jobs above came up
748
+ empty. The branch is still pushed either way. Only the current run counts: a
749
+ dependent that is empty, failed, on cooldown, or not selected does not
750
+ suppress anything. See
751
+ [ADR 0009](docs/adr/0009-unless-superseded-mr-creation.md) for details and
752
+ limitations.
610
753
 
611
754
  ### Example
612
755
 
@@ -718,9 +861,8 @@ title = "run the shared check script for this job"
718
861
 
719
862
  #### `RA_WORKSPACE_DIR`
720
863
 
721
- A job's command runs in a temporary jj workspace, which is also its working
722
- directory. `RA_WORKSPACE_DIR` names that directory explicitly, so a command
723
- that changes directory can still find its way back:
864
+ `RA_WORKSPACE_DIR` names the job's [temporary workspace](#how-it-works)
865
+ explicitly, so a command that changes directory can still find its way back:
724
866
 
725
867
  ```toml
726
868
  [job.build]
@@ -882,7 +1024,13 @@ job across several flags - put a new job's required fields in one expression
882
1024
  unknown key or malformed value is reported with the offending `--set`
883
1025
  argument named.
884
1026
 
885
- ## Selecting jobs with tags
1027
+ ## Run control
1028
+
1029
+ The sections below cover the run-control options from the
1030
+ [configuration reference](#jobname) in detail: which jobs a run selects, how
1031
+ to schedule and throttle them, and how to generate jobs dynamically.
1032
+
1033
+ ### Selecting jobs with tags
886
1034
 
887
1035
  Which jobs a `repoactive run` touches is decided by **tags**. Every job has
888
1036
  a set of tags, with a smart default:
@@ -917,8 +1065,8 @@ mistyped one.
917
1065
  Because assigning a tag removes the implicit `enabled` tag, **tags are
918
1066
  load-bearing, not free-form labels**: tagging a job takes it out of the bare
919
1067
  `repoactive run`. If you want a job to stay in the default run _and_ belong
920
- to a group, list both: `tags = ["enabled", "weekly"]`. (For MR/PR labels,
921
- use `labels` - a separate concept.)
1068
+ to a group, list both: `tags = ["enabled", "weekly"]`. (For MR labels, use
1069
+ `labels` - a separate concept.)
922
1070
 
923
1071
  Tag selection is _explicit_ selection, so - like naming a job - it ignores
924
1072
  the `enabled`/`disabled` defaults and force-includes dependencies. The bare
@@ -926,7 +1074,7 @@ the `enabled`/`disabled` defaults and force-includes dependencies. The bare
926
1074
  itself selected is dropped
927
1075
  (`==> [name] skipped (dependency not in default run)`).
928
1076
 
929
- ### Keeping unmerged branches current
1077
+ #### Keeping unmerged branches current
930
1078
 
931
1079
  The bare `repoactive run` additionally refreshes **any job that currently
932
1080
  has an unmerged branch**, regardless of its tags. A branch is "unmerged"
@@ -945,7 +1093,7 @@ don't have to wait for the next weekly run to resolve a conflict with
945
1093
  disabled job's unmerged branch is refreshed too: it was most likely created
946
1094
  by an explicit run, and letting it drift out of date helps no one.)
947
1095
 
948
- ## Disabling jobs
1096
+ ### Disabling jobs
949
1097
 
950
1098
  Set `disabled = true` on a `[job.<name>]` to keep it in the config but leave
951
1099
  it out of normal runs; it is exactly sugar for `tags = ["disabled"]` (so the
@@ -966,7 +1114,7 @@ disabled = true
966
1114
  `repoactive run --tag disabled`, which runs everything currently turned
967
1115
  off.
968
1116
 
969
- ## Running a job on a schedule
1117
+ ### Running a job on a schedule
970
1118
 
971
1119
  `repoactive` is not a daemon and has no built-in scheduler - the cadence of
972
1120
  a job is whatever cadence you invoke it with. To run a job on a fixed
@@ -994,7 +1142,7 @@ inferring a schedule from `repoactive`'s own history: real cron is stateful
994
1142
  and excludes the other days, whereas `repoactive` only ever sees what has
995
1143
  _landed_ (see `cooldown_period` below).
996
1144
 
997
- ### One run at a time per repository
1145
+ #### One run at a time per repository
998
1146
 
999
1147
  A `repoactive run` takes an exclusive per-repository lock for its duration,
1000
1148
  so two runs against the same repository never interleave (and corrupt each
@@ -1012,7 +1160,7 @@ The lock is an advisory `flock` on `.jj/repoactive.lock`; the OS releases it
1012
1160
  automatically if a run is killed, so a crashed run never leaves the
1013
1161
  repository locked.
1014
1162
 
1015
- ## Gating jobs with `run_only_if_changed`
1163
+ ### Gating jobs with `run_only_if_changed`
1016
1164
 
1017
1165
  `run_only_if_changed` lets a job declare that it should only run when
1018
1166
  specific upstream jobs actually produced a diff. If none of the named jobs
@@ -1046,12 +1194,15 @@ Key behaviour:
1046
1194
  - **Dependents are unaffected.** A skipped job records a no-op result (like
1047
1195
  a job on cooldown), so any job that `depends_on` it still runs on the base
1048
1196
  branch - the skip does not cascade.
1049
- - **No ordering constraint.** Names in `run_only_if_changed` do not have to
1050
- appear in `depends_on`. Any job that runs before this one in topological
1051
- order can be listed; in practice most usages name a direct dependency, as
1052
- in the example above.
1197
+ - **Watched jobs must run first.** A watched job does not have to appear in
1198
+ `depends_on`, but it must be ordered before this one so its result is
1199
+ known when the gate is evaluated - either as a (direct or transitive)
1200
+ `depends_on` ancestor, or simply earlier in the config file. Listing a job
1201
+ that runs later (or alongside, with no `depends_on` edge) is rejected at
1202
+ config load. In practice most usages name a direct dependency, as in the
1203
+ example above.
1053
1204
 
1054
- ## Throttling jobs with `cooldown_period`
1205
+ ### Throttling jobs with `cooldown_period`
1055
1206
 
1056
1207
  Every commit `repoactive` creates carries a `Repoactive-Job: <name>` trailer
1057
1208
  identifying the job that produced it. When a job sets `cooldown_period`,
@@ -1074,7 +1225,14 @@ runs: `repoactive` does not fetch, so a clone that has not pulled the merge
1074
1225
  will not see the cooldown and will re-run the job. See
1075
1226
  [Keeping the local clone current](#keeping-the-local-clone-current).
1076
1227
 
1077
- ## Throttling a job when a superset lands with `cooldown_on`
1228
+ Naming a job explicitly on the command line (`repoactive run <name>`)
1229
+ overrides its cooldown: the job runs now regardless of when it last landed,
1230
+ as if its `cooldown_period` were unset. The cooldown only ever gates the
1231
+ _default_ run and `--tag` selection, the automated paths it exists to
1232
+ throttle. Jobs pulled in only as dependencies of a named job are not
1233
+ themselves named, so they keep their own cooldowns.
1234
+
1235
+ ### Throttling a job when a superset lands with `cooldown_on`
1078
1236
 
1079
1237
  Some jobs are strict supersets of others. `uv lock --upgrade` refreshes
1080
1238
  every dependency group, so its diff already contains everything
@@ -1114,7 +1272,7 @@ has an open, unmerged branch is always refreshed regardless of cooldown (see
1114
1272
  its branch is rebased and, if the change is now redundant, self-closes on
1115
1273
  the next run.
1116
1274
 
1117
- ## Limiting job runtime with `timeout`
1275
+ ### Limiting job runtime with `timeout`
1118
1276
 
1119
1277
  A job's `command` can hang or run away. Setting `timeout` caps how long the
1120
1278
  command may run; when the limit is reached `repoactive` kills the command's
@@ -1144,7 +1302,7 @@ title = "chore: fix lint"
1144
1302
  timeout = "0s" # opt out of the timeout entirely
1145
1303
  ```
1146
1304
 
1147
- ## Generating jobs dynamically
1305
+ ### Generating jobs dynamically
1148
1306
 
1149
1307
  Sometimes the useful set of jobs depends on the repository's contents - one
1150
1308
  job per package in a monorepo, one per entry in a manifest - and you don't
@@ -1175,7 +1333,7 @@ title = "build: upgrade pkg-a"
1175
1333
 
1176
1334
  Fragments may only contain `[job.<name>]` tables; anything else (e.g.
1177
1335
  `[job-defaults]`) fails the generator. To set defaults for the emitted jobs,
1178
- set them on the generator itself — they are inherited (see below).
1336
+ set them on the generator itself - they are inherited (see below).
1179
1337
 
1180
1338
  Key points:
1181
1339
 
@@ -1184,10 +1342,10 @@ Key points:
1184
1342
  it emits.
1185
1343
  - **Inheritance with override.** Each emitted job inherits the generator's
1186
1344
  `tags`, `cooldown_period`, `base_branch`, `timeout`, `labels`,
1187
- `branch_prefix`/title prefixes, `draft` and `create_mr` unless its own
1188
- fragment sets them. It also defaults to `depends_on = ["<generator>"]`
1189
- (i.e. built flat on `trunk()`); override `depends_on` to a sibling emitted
1190
- job to stack them into an MR chain.
1345
+ `branch_prefix`/title prefixes, `draft`, `create_mr` and `auto_merge`
1346
+ unless its own fragment sets them. It also defaults to
1347
+ `depends_on = ["<generator>"]` (i.e. built flat on `trunk()`); override
1348
+ `depends_on` to a sibling emitted job to stack them into an MR chain.
1191
1349
  - **Stable names are your responsibility.** Cooldown, branches and the
1192
1350
  `Repoactive-Job` trailer all key on a job's `name`, so derive emitted
1193
1351
  names deterministically from repository state (`upgrade-pkg-a`, not a
@@ -1218,17 +1376,17 @@ design.
1218
1376
 
1219
1377
  ## Requirements
1220
1378
 
1221
- - Python 3.11 or later
1379
+ - A Unix-like operating system (Linux, macOS, BSD). `repoactive` uses POSIX
1380
+ process groups to enforce [`timeout`](#limiting-job-runtime-with-timeout)
1381
+ and `flock` for the
1382
+ [per-repository run lock](#one-run-at-a-time-per-repository); Windows is
1383
+ not supported.
1384
+ - Python 3.12 or later
1222
1385
  - [jj (Jujutsu)](https://github.com/jj-vcs/jj) - `repoactive` uses jj to
1223
1386
  manage branches and commits in the target repository
1224
1387
  - A configured jj user name and email - jj records them as the commit
1225
- author. Set them once with:
1226
-
1227
- ```bash
1228
- jj config set --user user.name "My Name"
1229
- jj config set --user user.email "me@example.com"
1230
- ```
1231
-
1388
+ author. Set them once with the `jj config set` commands shown in
1389
+ [Quick start](#quick-start) step 1.
1232
1390
  - A GitLab or GitHub API token exposed via the environment variable named in
1233
1391
  `platform.token_env` (default: `GITHUB_TOKEN` for GitHub.com,
1234
1392
  `GITLAB_TOKEN` for GitLab.com)
@@ -1256,3 +1414,12 @@ jj log -r 'repoactive_unmerged()'
1256
1414
  jj log -r 'repoactive() & committer_date(after:"2025-01-01")'
1257
1415
  jj log -r 'repoactive() & description(regex:"(?m)^Repoactive-Job: uv-lock-upgrade$")'
1258
1416
  ```
1417
+
1418
+ ## Changelog
1419
+
1420
+ User-facing changes are recorded in [CHANGELOG.md](CHANGELOG.md).
1421
+
1422
+ ## License
1423
+
1424
+ `repoactive` is licensed under the
1425
+ [GNU General Public License v3.0 or later](LICENSE).