repoactive 0.2.4.dev1__tar.gz → 0.2.5.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 (22) hide show
  1. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/PKG-INFO +298 -169
  2. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/README.md +297 -168
  3. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/pyproject.toml +1 -1
  4. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/src/repoactive/config.py +49 -0
  5. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/src/repoactive/jj.py +53 -53
  6. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/src/repoactive/platforms/gitlab.py +5 -2
  7. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/src/repoactive/runner.py +83 -43
  8. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/.gitignore +0 -0
  9. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/LICENSE +0 -0
  10. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/src/repoactive/__init__.py +0 -0
  11. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/src/repoactive/cli.py +0 -0
  12. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/src/repoactive/constants.py +0 -0
  13. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/src/repoactive/graph.py +0 -0
  14. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/src/repoactive/jobtree.py +0 -0
  15. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/src/repoactive/lock.py +0 -0
  16. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/src/repoactive/platforms/__init__.py +0 -0
  17. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/src/repoactive/platforms/base.py +0 -0
  18. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/src/repoactive/platforms/github.py +0 -0
  19. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/src/repoactive/progress.py +0 -0
  20. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/src/repoactive/settings.py +0 -0
  21. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/src/repoactive/ui.py +0 -0
  22. {repoactive-0.2.4.dev1 → repoactive-0.2.5.dev1}/src/repoactive/updates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: repoactive
3
- Version: 0.2.4.dev1
3
+ Version: 0.2.5.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
@@ -37,10 +37,11 @@ and (with `--mode publish`) the full MR lifecycle.
37
37
 
38
38
  ## Contents
39
39
 
40
+ - [Use cases](#use-cases)
40
41
  - [Installation](#installation)
41
42
  - [Quick start](#quick-start)
42
43
  - [How it works](#how-it-works)
43
- - [Use cases](#use-cases)
44
+ - [Keeping the local clone current](#keeping-the-local-clone-current)
44
45
  - [Usage](#usage)
45
46
  - [Environment variables](#environment-variables)
46
47
  - [Inspecting repoactive commits](#inspecting-repoactive-commits)
@@ -48,13 +49,35 @@ and (with `--mode publish`) the full MR lifecycle.
48
49
  - [Listing jobs](#listing-jobs)
49
50
  - [Listing tags](#listing-tags)
50
51
  - [Configuration](#configuration)
52
+ - [`[job-defaults]`](#job-defaults)
53
+ - [`[job.<name>]`](#jobname)
54
+ - [Stacking MRs](#stacking-mrs)
55
+ - [Example](#example)
56
+ - [`[platform.<name>]`](#platformname)
57
+ - [Config file locations](#config-file-locations)
58
+ - [Overriding values on the command line](#overriding-values-on-the-command-line)
51
59
  - [Selecting jobs with tags](#selecting-jobs-with-tags)
60
+ - [Keeping unmerged branches current](#keeping-unmerged-branches-current)
52
61
  - [Disabling jobs](#disabling-jobs)
62
+ - [Running a job on a schedule](#running-a-job-on-a-schedule)
63
+ - [One run at a time per repository](#one-run-at-a-time-per-repository)
53
64
  - [Gating jobs with `run_only_if_changed`](#gating-jobs-with-run_only_if_changed)
54
65
  - [Throttling jobs with `cooldown_period`](#throttling-jobs-with-cooldown_period)
66
+ - [Throttling a job when a superset lands with `cooldown_on`](#throttling-a-job-when-a-superset-lands-with-cooldown_on)
55
67
  - [Limiting job runtime with `timeout`](#limiting-job-runtime-with-timeout)
56
68
  - [Generating jobs dynamically](#generating-jobs-dynamically)
57
69
  - [Requirements](#requirements)
70
+ - [Appendix](#appendix)
71
+ - [jj revset aliases](#jj-revset-aliases)
72
+
73
+ ## Use cases
74
+
75
+ - Keeping generated files (API clients, protobuf bindings, lock files) in
76
+ sync with their sources
77
+ - Applying organization-wide refactors or policy changes across many
78
+ repositories
79
+ - Automating any periodic code transformation that should go through a
80
+ review process
58
81
 
59
82
  ## Installation
60
83
 
@@ -98,7 +121,7 @@ needs a GitHub or GitLab API token in the environment. See
98
121
  ```
99
122
 
100
123
  4. **Run the job locally.** In the default `local` mode nothing is pushed
101
- and no MR is created — repoactive just records the diff your script
124
+ and no MR is created - repoactive just records the diff your script
102
125
  produced on the branch `repoactive/uv-lock-upgrade` and prints a
103
126
  `jj op restore` command to undo the run:
104
127
 
@@ -109,7 +132,7 @@ needs a GitHub or GitLab API token in the environment. See
109
132
  5. **Publish it.** Put the API token your platform uses in the environment
110
133
  (`GITHUB_TOKEN` for GitHub.com, `GITLAB_TOKEN` for GitLab.com by
111
134
  default), fetch the latest base branch, then let repoactive push the
112
- branch and open — or update — the merge request:
135
+ branch and open - or update - the merge request:
113
136
 
114
137
  ```bash
115
138
  jj git fetch # repoactive never fetches on its own
@@ -147,7 +170,7 @@ need to write is the script that produces the change.
147
170
  3. If the script produced a diff, it records the change. With `--mode push`
148
171
  or `--mode publish`, it pushes the branch; with `--mode publish`, it also
149
172
  creates or updates the merge request. On a re-run, if the diff matches
150
- what is already on the branch, the commit and branch are left untouched —
173
+ what is already on the branch, the commit and branch are left untouched -
151
174
  avoiding an unnecessary push that would re-trigger CI pipelines. The
152
175
  commit is updated (and the branch pushed) only when the diff itself
153
176
  changes or when `title`/`commit_title_prefix` changed. Command output
@@ -161,7 +184,7 @@ need to write is the script that produces the change.
161
184
  > **jj commits the whole working tree.** Because `repoactive` uses jj, every
162
185
  > new file your script creates inside the working directory is added to the
163
186
  > commit unless it is git-ignored. There is no way to select which
164
- > working-tree changes become part of the commit — they all will. Keep
187
+ > working-tree changes become part of the commit - they all will. Keep
165
188
  > `.gitignore` up to date so build artifacts, caches, and other stray files
166
189
  > your script produces do not end up in the diff.
167
190
 
@@ -175,21 +198,12 @@ local clone advances past it.
175
198
 
176
199
  **Fetch before each run.** Run `jj git fetch` (or `git fetch --prune`) in
177
200
  the same cron job or CI pipeline that invokes `repoactive`, before it. If
178
- you do not, jobs rebase onto a stale base and — most importantly —
201
+ you do not, jobs rebase onto a stale base and - most importantly -
179
202
  [`cooldown_period`](#throttling-jobs-with-cooldown_period) never engages,
180
203
  because the commit that would trigger it has not reached the local base
181
204
  branch. See
182
205
  [ADR 0005](docs/adr/0005-local-repository-is-the-source-of-truth.md).
183
206
 
184
- ## Use cases
185
-
186
- - Keeping generated files (API clients, protobuf bindings, lock files) in
187
- sync with their sources
188
- - Applying organization-wide refactors or policy changes across many
189
- repositories
190
- - Automating any periodic code transformation that should go through a
191
- review process
192
-
193
207
  ## Usage
194
208
 
195
209
  ```bash
@@ -323,28 +337,6 @@ repoactive recent-commits --status unmerged
323
337
  | `--status [all\|merged\|unmerged]` | `-s` | Filter by merge status into trunk (default: `all`) |
324
338
  | `--debug` | `-d` | Enable debug logging |
325
339
 
326
- ### jj revset aliases
327
-
328
- To query repoactive commits directly in jj, add these aliases to your
329
- repository config (`jj config set --repo`) or your global config
330
- (`jj config set --user`):
331
-
332
- ```toml
333
- [revset-aliases]
334
- 'repoactive()' = 'description(regex:"(?m)^Repoactive-Job: ")'
335
- 'repoactive_merged()' = 'repoactive() & ::trunk()'
336
- 'repoactive_unmerged()' = 'repoactive() & ~(::trunk())'
337
- ```
338
-
339
- Then use them directly in jj:
340
-
341
- ```bash
342
- jj log -r 'repoactive()'
343
- jj log -r 'repoactive_unmerged()'
344
- jj log -r 'repoactive() & committer_date(after:"2025-01-01")'
345
- jj log -r 'repoactive() & description(regex:"(?m)^Repoactive-Job: uv-lock-upgrade$")'
346
- ```
347
-
348
340
  ## Validating configuration
349
341
 
350
342
  ```
@@ -372,12 +364,8 @@ code 1.
372
364
  Validation checks include unknown keys, missing required fields, invalid
373
365
  `depends_on` references, and circular job dependencies.
374
366
 
375
- | Option | Short | Description |
376
- | ------------------ | ----- | ---------------------------------------------------------------------------------------------------------------------------- |
377
- | `--config PATH` | `-c` | Config file or directory of `*.toml` files; repeat to merge. Default: `.repoactive.d/` and `.repoactive.toml` under `--repo` |
378
- | `--set NAME=VALUE` | `-s` | Override a config value (repeatable); `NAME` is a dotted TOML key, `VALUE` a TOML expression. Wins over `--config` |
379
- | `--repo PATH` | `-r` | jj repository path (default: `.`) |
380
- | `--debug` | `-d` | Enable debug logging |
367
+ The command accepts the same `--config`, `--set`, `--repo`, and `--debug`
368
+ options as [`repoactive run`](#usage).
381
369
 
382
370
  ## Listing jobs
383
371
 
@@ -403,12 +391,8 @@ build Build the project enabled
403
391
  └── deploy Deploy to staging nightly, risky
404
392
  ```
405
393
 
406
- | Option | Short | Description |
407
- | ------------------ | ----- | ---------------------------------------------------------------------------------------------------------------------------- |
408
- | `--config PATH` | `-c` | Config file or directory of `*.toml` files; repeat to merge. Default: `.repoactive.d/` and `.repoactive.toml` under `--repo` |
409
- | `--set NAME=VALUE` | `-s` | Override a config value (repeatable); `NAME` is a dotted TOML key, `VALUE` a TOML expression. Wins over `--config` |
410
- | `--repo PATH` | `-r` | jj repository path (default: `.`) |
411
- | `--debug` | `-d` | Enable debug logging |
394
+ The command accepts the same `--config`, `--set`, `--repo`, and `--debug`
395
+ options as [`repoactive run`](#usage).
412
396
 
413
397
  ## Listing tags
414
398
 
@@ -417,7 +401,7 @@ repoactive info tags [OPTIONS]
417
401
  ```
418
402
 
419
403
  Group the configured jobs by tag and print each tag with the jobs carrying
420
- it. Jobs are grouped by their effective tags — the tags driving job
404
+ it. Jobs are grouped by their effective tags - the tags driving job
421
405
  selection: a job's explicit `tags`, or the implicit `enabled`/`disabled` tag
422
406
  when it has none. Within each tag, jobs are shown as a dependency tree in
423
407
  topological order: a job is nested under its `depends_on` targets carrying
@@ -442,12 +426,8 @@ nightly:
442
426
  benchmark Run nightly benchmarks nightly
443
427
  ```
444
428
 
445
- | Option | Short | Description |
446
- | ------------------ | ----- | ---------------------------------------------------------------------------------------------------------------------------- |
447
- | `--config PATH` | `-c` | Config file or directory of `*.toml` files; repeat to merge. Default: `.repoactive.d/` and `.repoactive.toml` under `--repo` |
448
- | `--set NAME=VALUE` | `-s` | Override a config value (repeatable); `NAME` is a dotted TOML key, `VALUE` a TOML expression. Wins over `--config` |
449
- | `--repo PATH` | `-r` | jj repository path (default: `.`) |
450
- | `--debug` | `-d` | Enable debug logging |
429
+ The command accepts the same `--config`, `--set`, `--repo`, and `--debug`
430
+ options as [`repoactive run`](#usage).
451
431
 
452
432
  ## Configuration
453
433
 
@@ -456,135 +436,200 @@ passed via `--config`). See [Config file locations](#config-file-locations)
456
436
  for how the defaults are discovered and how to split the config across
457
437
  several files.
458
438
 
439
+ A minimal config with a single job:
440
+
441
+ ```toml
442
+ [job.uv-lock-upgrade]
443
+ command = "uv lock --upgrade"
444
+ title = "build: upgrade dependencies"
445
+ ```
446
+
459
447
  Every key in `[job-defaults]` supplies the default for the matching per-job
460
448
  key; any job may override it by setting the same key in its `[job.<name>]`
461
449
  block.
462
450
 
451
+ ### `[job-defaults]`
452
+
453
+ **MR/PR options:**
454
+
455
+ - **`labels`** (default: `[]`) - Labels applied to every MR/PR. Per-job
456
+ `labels` are merged with, not replaced by, this list.
457
+ - **`auto_merge`** (default: `false`) - When `true`, enable auto-merge on
458
+ every MR/PR so it merges automatically once its pipeline passes. On
459
+ GitHub, the repository must have "Allow auto-merge" enabled in its
460
+ settings.
461
+
462
+ **Branch and commit options:**
463
+
464
+ - **`branch_prefix`** (default: `"repoactive/"`) - Prefix prepended to the
465
+ job name to form the branch name.
466
+ - **`mr_title_prefix`** (default: `"[repoactive] "`) - Prefix prepended to
467
+ every MR/PR title. Set to `""` to disable.
468
+ - **`commit_title_prefix`** (default: `"[repoactive] "`) - Prefix prepended
469
+ to every commit title. Set to `""` to disable.
470
+ - **`base_branch`** (default: repo default) - Target branch for all jobs
471
+ that do not set their own.
472
+
473
+ **Run control:**
474
+
475
+ - **`cooldown_period`** (default: none) - Minimum time between a landed
476
+ change and the next run for any job. Format: `<number><unit>` where unit
477
+ is `s`, `m`, `h`, `d`, or `w` (e.g. `"7d"`). See
478
+ [Throttling jobs with `cooldown_period`](#throttling-jobs-with-cooldown_period).
479
+ - **`timeout`** (default: `"2m"`) - Maximum runtime for a job's command.
480
+ Same format as `cooldown_period`. See
481
+ [Limiting job runtime with `timeout`](#limiting-job-runtime-with-timeout).
482
+
483
+ ### `[job.<name>]`
484
+
485
+ The table key is the job's unique name; the branch is always
486
+ `branch_prefix + name`. Job names may contain letters, digits, hyphens, and
487
+ underscores.
488
+
489
+ **Required:**
490
+
491
+ - **`command`** - Shell command (or executable path) run in the repository
492
+ working directory. A non-zero exit is a failure.
493
+ - **`title`** - MR/PR title (also the commit subject, after
494
+ `commit_title_prefix`).
495
+
496
+ **MR/PR options:**
497
+
498
+ - **`description`** - Body text of the MR/PR.
499
+ - **`labels`** (default: `[]`) - Extra labels for this job's MR/PR, merged
500
+ with `job-defaults.labels`.
501
+ - **`draft`** (default: `false`) - Open the MR/PR as a draft. On GitHub,
502
+ draft state cannot be changed after creation.
503
+ - **`create_mr`** (default: `true`) - Whether to create an MR/PR: `true`
504
+ (always), `false` (push the branch but skip the MR), or
505
+ `"unless-superseded"` (skip when a dependent's MR from the same run
506
+ already contains this job's changes - see [Stacking MRs](#stacking-mrs)).
507
+ - **`auto_merge`** (default: inherited from `job-defaults`) - When `true`,
508
+ enable auto-merge on this job's MR/PR.
509
+
510
+ **Branch and commit options:**
511
+
512
+ - **`base_branch`** (default: inherited) - Target branch for this job's
513
+ MR/PR.
514
+ - **`branch_prefix`** (default: inherited) - Override the branch-name prefix
515
+ for this job only.
516
+ - **`mr_title_prefix`** (default: inherited) - Override the MR/PR title
517
+ prefix for this job only.
518
+ - **`commit_title_prefix`** (default: inherited) - Override the commit title
519
+ prefix for this job only.
520
+ - **`output_in_commit`** (default: `true`) - Append the job's command and
521
+ its captured output to the commit message. Set to `false` to keep the
522
+ commit message clean.
523
+
524
+ **Run control:**
525
+
526
+ - **`disabled`** (default: `false`) - Exclude this job from the bare
527
+ `repoactive run`. Sugar for `tags = ["disabled"]`; mutually exclusive with
528
+ `tags`. See [Disabling jobs](#disabling-jobs).
529
+ - **`tags`** (default: none) - Tags for job selection. A job with no tags
530
+ carries the implicit `enabled` tag and runs in the bare `repoactive run`;
531
+ setting any explicit tag removes `enabled`. See
532
+ [Selecting jobs with tags](#selecting-jobs-with-tags).
533
+ - **`depends_on`** (default: `[]`) - Jobs whose output this job builds on.
534
+ See [Stacking MRs](#stacking-mrs).
535
+ - **`run_only_if_changed`** (default: `[]`) - Only run this job if at least
536
+ one listed job produced a diff in the current run. See
537
+ [Gating jobs with `run_only_if_changed`](#gating-jobs-with-run_only_if_changed).
538
+ - **`cooldown_period`** (default: inherited) - Minimum time between a landed
539
+ change and the next run. See
540
+ [Throttling jobs with `cooldown_period`](#throttling-jobs-with-cooldown_period).
541
+ - **`cooldown_on`** (default: `[]`) - Broader jobs that subsume this one; a
542
+ recent landing of any of them also throttles this job. Requires a
543
+ `cooldown_period`. See
544
+ [Throttling a job when a superset lands with `cooldown_on`](#throttling-a-job-when-a-superset-lands-with-cooldown_on).
545
+ - **`timeout`** (default: inherited) - Maximum runtime for this job's
546
+ command. Set to `"0s"` to disable the timeout entirely. See
547
+ [Limiting job runtime with `timeout`](#limiting-job-runtime-with-timeout).
548
+ - **`emits_jobs`** (default: `false`) - Generator job: the command writes
549
+ `*.toml` job fragments into `$REPOACTIVE_JOBS_DIR` instead of producing a
550
+ diff. See [Generating jobs dynamically](#generating-jobs-dynamically).
551
+
552
+ ### Stacking MRs
553
+
554
+ When `depends_on` is set, `repoactive` starts the job's script from a
555
+ working tree that already contains all listed dependencies' diffs, rather
556
+ than from the plain base branch. The resulting MR branch includes both the
557
+ dependency changes and the new job's changes on top, and links to the parent
558
+ MRs are automatically added to the MR description.
559
+
560
+ Because a dependent's MR already contains its dependencies' changes, a
561
+ dependency chain normally opens one MR per job that produced a diff. Setting
562
+ `create_mr = "unless-superseded"` on the earlier jobs collapses that: such a
563
+ job skips its MR when a dependent job produced an MR in the same run, so the
564
+ chain yields a single MR on the topmost job that actually changed something
565
+
566
+ - falling back to the job below it when the jobs above came up empty. The
567
+ branch is still pushed either way. Only the current run counts: a
568
+ dependent that is empty, failed, on cooldown, or not selected does not
569
+ suppress anything. See
570
+ [ADR 0009](docs/adr/0009-unless-superseded-mr-creation.md) for details and
571
+ limitations.
572
+
573
+ ### Example
574
+
463
575
  ```toml
464
576
  [job-defaults]
465
- # Prefix prepended to job.name to form the branch name
466
- branch_prefix = "repoactive/"
467
- # Prefix prepended to every MR/PR title (set to "" to disable)
468
- mr_title_prefix = "[repoactive] "
469
- # Prefix prepended to every commit title (set to "" to disable)
470
- commit_title_prefix = "[repoactive] "
471
- # Labels applied to every MR/PR unless overridden per job
472
- labels = ["repoactive"]
473
- # Optional: default target branch for jobs that do not set their own
474
- # (default: repo default branch)
475
577
  base_branch = "main"
476
- # Optional: default cooldown_period applied to jobs that do not set their own
477
- # (default: none). See "Throttling jobs with cooldown_period" below.
578
+ labels = ["repoactive"]
478
579
  cooldown_period = "7d"
479
- # Optional: default timeout applied to jobs that do not set their own
480
- # (default: "2m"). See "Limiting job runtime with timeout" below.
481
- timeout = "1h"
580
+ auto_merge = true
581
+ branch_prefix = "bot/"
582
+ commit_title_prefix = "[bot] "
583
+ timeout = "10m"
482
584
 
483
- # The table key is the job's unique name; the branch is always
484
- # <branch_prefix><name>.
485
585
  [job.regenerate-api-client]
486
- # Script run in the repo working directory; non-zero exit = failure
487
586
  command = "python scripts/regen_api.py"
488
- # MR/PR title
489
587
  title = "api: regenerate API client"
490
- # Optional: MR description
491
588
  description = "Automated regeneration of the API client from the OpenAPI spec."
492
- # Optional: extra labels (merged with job-defaults.labels)
493
- labels = ["automated", "api"]
494
- # Optional: target branch (default: repo default branch)
495
- base_branch = "main"
496
- # Optional: open the MR/PR as a draft (default: false)
497
- draft = false
498
- # Optional: create an MR/PR for this job (default: true). Set to false to
499
- # push the branch without opening an MR/PR, or to "unless-superseded" to skip
500
- # the MR/PR when a dependent job's MR from the same run already contains this
501
- # job's changes (see the notes on depends_on below).
502
- create_mr = true
503
- # Optional: append the job's command and its output to the commit message
504
- # (default: true). Set to false to keep the commit message clean.
505
- output_in_commit = true
506
- # Optional: skip this job on "run all" invocations (default: false). Sugar for
507
- # tags = ["disabled"]; mutually exclusive with tags. See "Disabling jobs" below.
508
- disabled = false
509
- # Optional: tags for job selection (default: none -> the job carries the
510
- # implicit "enabled" tag and runs in the bare `repoactive run`). Setting any tag
511
- # removes "enabled", so the job runs only via `repoactive run --tag <tag>`. See
512
- # "Selecting jobs with tags" below.
513
- # tags = ["nightly"]
514
- # Optional: override branch_prefix/mr_title_prefix/commit_title_prefix from
515
- # job-defaults for this job only.
589
+ labels = ["api"]
516
590
  mr_title_prefix = "[api] "
517
- # Optional: only run this job if at least one of the named jobs produced a
518
- # diff in the current run. If none of them changed anything, this job is
519
- # skipped (with a no-op result so dependents still run). Names need not be
520
- # in depends_on; any job that ran before this one can be listed.
521
- # run_only_if_changed = ["other-job"]
522
- # Optional: minimum time between landed changes for this job. If a commit
523
- # from this job landed on the base branch within this window, the job is
524
- # skipped. Format: <number><unit>, unit one of s, m, h, d, w (e.g. "7d").
525
- cooldown_period = "7d"
526
- # Optional: maximum runtime for this job's command. When it expires the
527
- # command's process group is killed and the job fails. Same format as
528
- # cooldown_period.
529
- timeout = "30m"
591
+ create_mr = "unless-superseded"
530
592
 
531
593
  [job.sync-license-headers]
532
594
  command = "./scripts/add_license_headers.sh"
533
595
  title = "sync license headers"
596
+ draft = true
534
597
 
535
598
  [job.integration-tests-update]
536
599
  command = "./scripts/update_integration_tests.py"
537
600
  title = "tests: update integration tests"
538
- # Optional: run this job on top of the merged output of the listed jobs
539
601
  depends_on = ["regenerate-api-client", "sync-license-headers"]
602
+ timeout = "30m"
540
603
  ```
541
604
 
605
+ ### `[platform.<name>]`
606
+
542
607
  For public GitHub.com or GitLab.com repositories no platform declaration is
543
- needed — `repoactive` detects the remote URL automatically. To use a
608
+ needed - `repoactive` detects the remote URL automatically. To use a
544
609
  self-hosted instance, add a `[platform.<name>]` section (the name is a label
545
610
  of your choosing; platforms are matched to a repository by their `url`):
546
611
 
612
+ - **`url`** - Base URL of the platform instance (e.g.
613
+ `"https://gitlab.example.com"`).
614
+ - **`type`** - `"github"` or `"gitlab"`.
615
+ - **`token_env`** - Name of the environment variable holding the API token.
616
+
547
617
  ```toml
548
618
  [platform.company-gitlab]
549
- # Base URL of the platform instance
550
619
  url = "https://gitlab.example.com"
551
- # Name of the env var holding the API token
552
620
  token_env = "GITLAB_TOKEN"
553
- # type must be either "github" or "gitlab"
554
621
  type = "gitlab"
555
622
  ```
556
623
 
557
- The branch for each job is always `branch_prefix + job.name`, where
558
- `branch_prefix` is the job's own value if set, otherwise
559
- `job-defaults.branch_prefix`. Secrets are kept out of the config file by
560
- referencing environment variable names rather than inline values.
561
-
562
- The token named by `token_env` is **stripped from the environment job
563
- commands run in**, so a script cannot read the credential `repoactive` uses
564
- to push and create MRs. A job that needs its own credential must be given a
565
- separate one. `repoactive` otherwise trusts job commands — they run
566
- arbitrary code against the working tree — so the trust boundary is the
567
- config that defines them; see
624
+ Secrets are kept out of the config file by referencing environment variable
625
+ names rather than inline values. The token named by `token_env` is
626
+ **stripped from the environment job commands run in**, so a script cannot
627
+ read the credential `repoactive` uses to push and create MRs. A job that
628
+ needs its own credential must be given a separate one. `repoactive`
629
+ otherwise trusts job commands - they run arbitrary code against the working
630
+ tree - so the trust boundary is the config that defines them; see
568
631
  [ADR 0006](docs/adr/0006-job-commands-are-trusted.md).
569
632
 
570
- When `depends_on` is set, `repoactive` starts the job's script from a
571
- working tree that has all listed dependency branches merged together, rather
572
- than from the plain base branch. The resulting MR branch will therefore
573
- include both the dependency jobs and the new job on top. Links to the parent
574
- MRs are automatically added to the MR description.
575
-
576
- Because a dependent's MR already contains its dependencies' changes, a
577
- dependency chain normally opens one MR per job that produced a diff. Setting
578
- `create_mr = "unless-superseded"` on the earlier jobs collapses that: such a
579
- job skips its MR whenever a dependent job produced an MR in the same run, so
580
- the chain yields a single MR on the topmost job that actually changed
581
- something — falling back to the job below it when the jobs above came up
582
- empty. The branch is still pushed either way. Only the current run counts: a
583
- dependent that is empty, failed, on cooldown, or not selected does not
584
- suppress anything. See
585
- [ADR 0009](docs/adr/0009-unless-superseded-mr-creation.md) for details and
586
- limitations.
587
-
588
633
  ### Config file locations
589
634
 
590
635
  When no `--config`/`-c` option is given, `repoactive` looks for
@@ -609,8 +654,8 @@ directory, not `--repo`.
609
654
 
610
655
  `--set NAME=VALUE`/`-s` tweaks individual config values without editing a
611
656
  file. It is merged as the last, highest-priority source, so it wins over
612
- everything discovered or passed with `--config`. `NAME` is a TOML key —
613
- dotted keys reach into tables — and `VALUE` is a TOML expression, so strings
657
+ everything discovered or passed with `--config`. `NAME` is a TOML key -
658
+ dotted keys reach into tables - and `VALUE` is a TOML expression, so strings
614
659
  need quoting:
615
660
 
616
661
  ```bash
@@ -635,7 +680,7 @@ which then needs the full set of fields (`url`, `type`, `token_env`).
635
680
  `--set` is available on `run`, `validate-config`, `info jobs`, and
636
681
  `info tags`. Each `--set` is validated as its own source (like a separate
637
682
  config file), so it can amend existing jobs but cannot introduce a brand-new
638
- job across several flags — put a new job's required fields in one expression
683
+ job across several flags - put a new job's required fields in one expression
639
684
  (e.g. `--set 'job.x = {command = "…", title = "…"}'`) or a config file. An
640
685
  unknown key or malformed value is reported with the offending `--set`
641
686
  argument named.
@@ -648,14 +693,14 @@ a set of tags, with a smart default:
648
693
  - a plain job (no `tags`, not `disabled`) carries the implicit `enabled`
649
694
  tag;
650
695
  - `disabled = true` is sugar for `tags = ["disabled"]`;
651
- - setting `tags = [...]` uses exactly those tags — and, importantly, **drops
696
+ - setting `tags = [...]` uses exactly those tags - and, importantly, **drops
652
697
  the implicit `enabled` tag** unless you list it yourself.
653
698
 
654
699
  `repoactive run` with no arguments is shorthand for
655
700
  `repoactive run --tag enabled`: it runs every job carrying `enabled`. Pass
656
701
  `--tag`/`-t` (repeatable) to select a different set; a job runs if it
657
702
  carries **any** of the requested tags. Naming jobs and passing tags can be
658
- combined — the selection is the union of the two.
703
+ combined - the selection is the union of the two.
659
704
 
660
705
  ```bash
661
706
  # Run all jobs tagged "weekly" (regardless of whether they also have "enabled")
@@ -666,7 +711,7 @@ repoactive run --tag nightly regenerate-api-client
666
711
  ```
667
712
 
668
713
  Requesting a tag that no job carries is an error, exactly like naming an
669
- unknown job — a typo in a crontab's `--tag` fails loudly instead of silently
714
+ unknown job - a typo in a crontab's `--tag` fails loudly instead of silently
670
715
  running zero jobs. This also means a scheduled entry whose tag has lost its
671
716
  last member fails until you remove the entry (or re-tag a job); a tag only
672
717
  exists as a value on jobs, so an empty tag is indistinguishable from a
@@ -676,9 +721,9 @@ Because assigning a tag removes the implicit `enabled` tag, **tags are
676
721
  load-bearing, not free-form labels**: tagging a job takes it out of the bare
677
722
  `repoactive run`. If you want a job to stay in the default run _and_ belong
678
723
  to a group, list both: `tags = ["enabled", "weekly"]`. (For MR/PR labels,
679
- use `labels` — a separate concept.)
724
+ use `labels` - a separate concept.)
680
725
 
681
- Tag selection is _explicit_ selection, so — like naming a job — it ignores
726
+ Tag selection is _explicit_ selection, so - like naming a job - it ignores
682
727
  the `enabled`/`disabled` defaults and force-includes dependencies. The bare
683
728
  `repoactive run` is _implicit_ selection: a job whose dependency is not
684
729
  itself selected is dropped
@@ -696,7 +741,7 @@ latest `trunk()` and the command is re-run against it. (With
696
741
  `--mode push` it is just a branch.)
697
742
 
698
743
  This means a job's schedule tag governs when a _new_ branch is created,
699
- while the default run keeps an existing branch rebased and current — you
744
+ while the default run keeps an existing branch rebased and current - you
700
745
  don't have to wait for the next weekly run to resolve a conflict with
701
746
  `trunk()`. Once the branch lands, its commit becomes an ancestor of
702
747
  `trunk()`, so the job drops back to its normal tag-driven cadence. (A
@@ -710,6 +755,13 @@ it out of normal runs; it is exactly sugar for `tags = ["disabled"]` (so the
710
755
  two are mutually exclusive). The flag only affects the bare
711
756
  `repoactive run`:
712
757
 
758
+ ```toml
759
+ [job.experimental-refactor]
760
+ command = "./scripts/refactor.sh"
761
+ title = "chore: apply experimental refactor"
762
+ disabled = true
763
+ ```
764
+
713
765
  - On `repoactive run`, disabled jobs are skipped, along with any job that
714
766
  `depends_on` one (its dependency would not be produced).
715
767
  - Naming a job explicitly overrides it: `repoactive run my-job` runs
@@ -717,13 +769,13 @@ two are mutually exclusive). The flag only affects the bare
717
769
  `repoactive run --tag disabled`, which runs everything currently turned
718
770
  off.
719
771
 
720
- ### Running a job on a schedule
772
+ ## Running a job on a schedule
721
773
 
722
- `repoactive` is not a daemon and has no built-in scheduler — the cadence of
774
+ `repoactive` is not a daemon and has no built-in scheduler - the cadence of
723
775
  a job is whatever cadence you invoke it with. To run a job on a fixed
724
776
  schedule, tag it and have an OS cron job select that tag. The tag keeps the
725
777
  job out of the bare `repoactive run`, and the crontab decides the membership
726
- in one place — add or remove `weekly` jobs by editing the config, not the
778
+ in one place - add or remove `weekly` jobs by editing the config, not the
727
779
  crontab:
728
780
 
729
781
  ```toml
@@ -740,18 +792,18 @@ tags = ["weekly"]
740
792
  ```
741
793
 
742
794
  Because the cron is the sole trigger, the command runs exactly when cron
743
- fires — once, whether or not it produces a diff. This is more reliable than
795
+ fires - once, whether or not it produces a diff. This is more reliable than
744
796
  inferring a schedule from `repoactive`'s own history: real cron is stateful
745
797
  and excludes the other days, whereas `repoactive` only ever sees what has
746
798
  _landed_ (see `cooldown_period` below).
747
799
 
748
- #### One run at a time per repository
800
+ ### One run at a time per repository
749
801
 
750
802
  A `repoactive run` takes an exclusive per-repository lock for its duration,
751
803
  so two runs against the same repository never interleave (and corrupt each
752
804
  other's branches and temporary workspaces). If a run is started while
753
- another is still in progress — a slow run overrunning the next cron tick,
754
- say — the second one **exits immediately with status code 2** instead of
805
+ another is still in progress - a slow run overrunning the next cron tick,
806
+ say - the second one **exits immediately with status code 2** instead of
755
807
  waiting or racing. That code is distinct from the generic failure code (1),
756
808
  so a wrapper can treat "already running" as benign:
757
809
 
@@ -786,7 +838,7 @@ tags = ["weekly"]
786
838
  Here `prek-run-all` is only useful when `prek-autoupdate` changed something:
787
839
  if `prek-autoupdate` found no hooks to update, running `prek-run-all` would
788
840
  produce an empty diff anyway. With `run_only_if_changed`, the skip is
789
- explicit and immediate — `prek-run-all` never even starts.
841
+ explicit and immediate - `prek-run-all` never even starts.
790
842
 
791
843
  Key behaviour:
792
844
 
@@ -796,7 +848,7 @@ Key behaviour:
796
848
  skipped, it is treated as having produced no diff.
797
849
  - **Dependents are unaffected.** A skipped job records a no-op result (like
798
850
  a job on cooldown), so any job that `depends_on` it still runs on the base
799
- branch — the skip does not cascade.
851
+ branch - the skip does not cascade.
800
852
  - **No ordering constraint.** Names in `run_only_if_changed` do not have to
801
853
  appear in `depends_on`. Any job that runs before this one in topological
802
854
  order can be listed; in practice most usages name a direct dependency, as
@@ -825,6 +877,44 @@ runs: `repoactive` does not fetch, so a clone that has not pulled the merge
825
877
  will not see the cooldown and will re-run the job. See
826
878
  [Keeping the local clone current](#keeping-the-local-clone-current).
827
879
 
880
+ ## Throttling a job when a superset lands with `cooldown_on`
881
+
882
+ Some jobs are strict supersets of others. `uv lock --upgrade` refreshes
883
+ every dependency group, so its diff already contains everything
884
+ `uv lock --upgrade-group=dev` would produce. Re-running the narrower job
885
+ right after the broad one landed just opens a redundant MR.
886
+
887
+ Listing the broader jobs in `cooldown_on` widens the narrow job's cooldown
888
+ check: it is throttled when **its own trailer or any listed job's trailer**
889
+ last landed within its `cooldown_period`. So once the full upgrade lands,
890
+ the group-scoped job stays quiet for the cooldown window instead of
891
+ re-running.
892
+
893
+ ```toml
894
+ [job.full-lock]
895
+ command = "uv lock --upgrade"
896
+ title = "chore: update all dependencies"
897
+ cooldown_period = "7d"
898
+
899
+ [job.dev-lock]
900
+ command = "uv lock --upgrade-group=dev"
901
+ title = "chore: update dev dependencies"
902
+ cooldown_period = "7d"
903
+ cooldown_on = ["full-lock"]
904
+ ```
905
+
906
+ `cooldown_on` requires a `cooldown_period` on the same job (its own or one
907
+ inherited from `[job-defaults]`); without a window there is nothing to
908
+ throttle against, so a config that sets one without the other is rejected.
909
+ The relationship is one-directional and needs no change to the broad job -
910
+ it still writes only its own trailer.
911
+
912
+ This only suppresses _starting_ a redundant run. A narrow job that already
913
+ has an open, unmerged branch is always refreshed regardless of cooldown (see
914
+ [Keeping unmerged branches current](#keeping-unmerged-branches-current)), so
915
+ its branch is rebased and, if the change is now redundant, self-closes on
916
+ the next run.
917
+
828
918
  ## Limiting job runtime with `timeout`
829
919
 
830
920
  A job's `command` can hang or run away. Setting `timeout` caps how long the
@@ -840,10 +930,25 @@ zero duration (`timeout = "0s"`) disables the timeout entirely; since TOML
840
930
  has no null value, this is how a job opts out of a timeout set in
841
931
  `job-defaults`.
842
932
 
933
+ ```toml
934
+ [job-defaults]
935
+ timeout = "10m" # raise the built-in 2m default for all jobs
936
+
937
+ [job.slow-codegen]
938
+ command = "./scripts/slow_codegen.sh"
939
+ title = "chore: regenerate bindings"
940
+ timeout = "30m" # this job needs extra time
941
+
942
+ [job.quick-lint]
943
+ command = "ruff check ."
944
+ title = "chore: fix lint"
945
+ timeout = "0s" # opt out of the timeout entirely
946
+ ```
947
+
843
948
  ## Generating jobs dynamically
844
949
 
845
- Sometimes the useful set of jobs depends on the repository's contents — one
846
- job per package in a monorepo, one per entry in a manifest — and you don't
950
+ Sometimes the useful set of jobs depends on the repository's contents - one
951
+ job per package in a monorepo, one per entry in a manifest - and you don't
847
952
  want to hand-maintain them. A **generator** is an ordinary `[job.<name>]`
848
953
  with `emits_jobs = true`. Instead of producing a diff, its command writes
849
954
  one or more `*.toml` job fragments into the directory named by the
@@ -895,7 +1000,7 @@ Key points:
895
1000
  > only re-run when the generator re-emits it, and the generator is gated by
896
1001
  > the same landed commits (via the shared trailer). So overriding an emitted
897
1002
  > job's `cooldown_period` only matters when you make it **longer** than the
898
- > generator's — then the job stays throttled even after the generator has
1003
+ > generator's - then the job stays throttled even after the generator has
899
1004
  > run again. Making it **shorter** has no effect: while the generator is on
900
1005
  > its own cooldown the job is never re-emitted, and by the time the
901
1006
  > generator runs again the job's shorter window has long since elapsed
@@ -924,3 +1029,27 @@ design.
924
1029
  - A GitLab or GitHub API token exposed via the environment variable named in
925
1030
  `platform.token_env` (default: `GITHUB_TOKEN` for GitHub.com,
926
1031
  `GITLAB_TOKEN` for GitLab.com)
1032
+
1033
+ ## Appendix
1034
+
1035
+ ### jj revset aliases
1036
+
1037
+ To query repoactive commits directly in jj, add these aliases to your
1038
+ repository config (`jj config set --repo`) or your global config
1039
+ (`jj config set --user`):
1040
+
1041
+ ```toml
1042
+ [revset-aliases]
1043
+ 'repoactive()' = 'description(regex:"(?m)^Repoactive-Job: ")'
1044
+ 'repoactive_merged()' = 'repoactive() & ::trunk()'
1045
+ 'repoactive_unmerged()' = 'repoactive() & ~(::trunk())'
1046
+ ```
1047
+
1048
+ Then use them directly in jj:
1049
+
1050
+ ```bash
1051
+ jj log -r 'repoactive()'
1052
+ jj log -r 'repoactive_unmerged()'
1053
+ jj log -r 'repoactive() & committer_date(after:"2025-01-01")'
1054
+ jj log -r 'repoactive() & description(regex:"(?m)^Repoactive-Job: uv-lock-upgrade$")'
1055
+ ```