repoactive 0.2.8.dev2__tar.gz → 0.3.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.
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/PKG-INFO +123 -11
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/README.md +122 -10
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/pyproject.toml +1 -1
- repoactive-0.3.0/src/repoactive/boxquote.py +51 -0
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/src/repoactive/config.py +91 -5
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/src/repoactive/runner.py +71 -6
- repoactive-0.2.8.dev2/src/repoactive/boxquote.py +0 -32
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/.gitignore +0 -0
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/LICENSE +0 -0
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/src/repoactive/__init__.py +0 -0
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/src/repoactive/cli.py +0 -0
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/src/repoactive/constants.py +0 -0
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/src/repoactive/graph.py +0 -0
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/src/repoactive/jj.py +0 -0
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/src/repoactive/jobtree.py +0 -0
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/src/repoactive/lock.py +0 -0
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/src/repoactive/platforms/__init__.py +0 -0
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/src/repoactive/platforms/base.py +0 -0
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/src/repoactive/platforms/github.py +0 -0
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/src/repoactive/platforms/gitlab.py +0 -0
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/src/repoactive/progress.py +0 -0
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/src/repoactive/selection.py +0 -0
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/src/repoactive/settings.py +0 -0
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/src/repoactive/trailers.py +0 -0
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/src/repoactive/ui.py +0 -0
- {repoactive-0.2.8.dev2 → repoactive-0.3.0}/src/repoactive/updates.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: repoactive
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -55,6 +55,7 @@ and (with `--mode publish`) the full MR lifecycle.
|
|
|
55
55
|
- [Example](#example)
|
|
56
56
|
- [`[platform.<name>]`](#platformname)
|
|
57
57
|
- [Config file locations](#config-file-locations)
|
|
58
|
+
- [Variables passed to job commands](#variables-passed-to-job-commands)
|
|
58
59
|
- [Overriding values on the command line](#overriding-values-on-the-command-line)
|
|
59
60
|
- [Selecting jobs with tags](#selecting-jobs-with-tags)
|
|
60
61
|
- [Keeping unmerged branches current](#keeping-unmerged-branches-current)
|
|
@@ -467,13 +468,17 @@ block.
|
|
|
467
468
|
**Branch and commit options:**
|
|
468
469
|
|
|
469
470
|
- **`branch_prefix`** (default: `"repoactive/"`) - Prefix prepended to the
|
|
470
|
-
job name to form the branch name.
|
|
471
|
+
job name to form the branch name. Set to `""` to use the job name alone.
|
|
472
|
+
With an empty prefix the branch name is exactly the job name, so make sure
|
|
473
|
+
no job is named after your base branch (e.g. a job named `main`) - the two
|
|
474
|
+
branches would otherwise collide.
|
|
471
475
|
- **`mr_title_prefix`** (default: `"[repoactive] "`) - Prefix prepended to
|
|
472
476
|
every MR/PR title. Set to `""` to disable.
|
|
473
477
|
- **`commit_title_prefix`** (default: `"[repoactive] "`) - Prefix prepended
|
|
474
478
|
to every commit title. Set to `""` to disable.
|
|
475
479
|
- **`base_branch`** (default: repo default) - Target branch for all jobs
|
|
476
|
-
that do not set their own.
|
|
480
|
+
that do not set their own. May also be a jj revset expression such as
|
|
481
|
+
`trunk()`, `root()`, or a user-defined revset alias.
|
|
477
482
|
|
|
478
483
|
**Run control:**
|
|
479
484
|
|
|
@@ -484,6 +489,10 @@ block.
|
|
|
484
489
|
- **`timeout`** (default: `"2m"`) - Maximum runtime for a job's command.
|
|
485
490
|
Same format as `cooldown_period`. See
|
|
486
491
|
[Limiting job runtime with `timeout`](#limiting-job-runtime-with-timeout).
|
|
492
|
+
- **`shell`** (default: `/bin/sh`) - Interpreter used to run job commands. A
|
|
493
|
+
bare name is resolved on `PATH` (e.g. `"bash"`); an absolute path also
|
|
494
|
+
works. The command runs as `<shell> -c <command>`. Arguments are not
|
|
495
|
+
allowed - the value must be a single binary.
|
|
487
496
|
|
|
488
497
|
### `[job.<name>]`
|
|
489
498
|
|
|
@@ -518,7 +527,8 @@ underscores.
|
|
|
518
527
|
**Branch and commit options:**
|
|
519
528
|
|
|
520
529
|
- **`base_branch`** (default: inherited) - Target branch for this job's
|
|
521
|
-
MR/PR.
|
|
530
|
+
MR/PR. May also be a jj revset expression such as `trunk()`, `root()`, or
|
|
531
|
+
a user-defined revset alias.
|
|
522
532
|
- **`branch_prefix`** (default: inherited) - Override the branch-name prefix
|
|
523
533
|
for this job only.
|
|
524
534
|
- **`mr_title_prefix`** (default: inherited) - Override the MR/PR title
|
|
@@ -553,9 +563,12 @@ underscores.
|
|
|
553
563
|
- **`timeout`** (default: inherited) - Maximum runtime for this job's
|
|
554
564
|
command. Set to `"0s"` to disable the timeout entirely. See
|
|
555
565
|
[Limiting job runtime with `timeout`](#limiting-job-runtime-with-timeout).
|
|
566
|
+
- **`shell`** (default: inherited) - Interpreter used to run this job's
|
|
567
|
+
command, overriding `job-defaults.shell`. See
|
|
568
|
+
[`job-defaults.shell`](#job-defaults).
|
|
556
569
|
- **`emits_jobs`** (default: `false`) - Generator job: the command writes
|
|
557
|
-
`*.toml` job fragments into `$
|
|
558
|
-
|
|
570
|
+
`*.toml` job fragments into `$RA_JOBS_DIR` instead of producing a diff.
|
|
571
|
+
See [Generating jobs dynamically](#generating-jobs-dynamically).
|
|
559
572
|
|
|
560
573
|
### Stacking MRs
|
|
561
574
|
|
|
@@ -658,6 +671,105 @@ directory of `*.toml` files, and may be repeated to merge several sources;
|
|
|
658
671
|
later sources win. Explicit paths are resolved relative to the current
|
|
659
672
|
directory, not `--repo`.
|
|
660
673
|
|
|
674
|
+
### Variables passed to job commands
|
|
675
|
+
|
|
676
|
+
`repoactive` injects a few `RA_`-prefixed environment variables into every
|
|
677
|
+
job command's environment. These are distinct from the `REPOACTIVE_`
|
|
678
|
+
variables [above](#environment-variables), which configure `repoactive`
|
|
679
|
+
itself.
|
|
680
|
+
|
|
681
|
+
| Variable | Value | Set when |
|
|
682
|
+
| ---------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
683
|
+
| `RA_JOB_NAME` | The name of the job the command belongs to. | Always. |
|
|
684
|
+
| `RA_WORKSPACE_DIR` | The temporary jj workspace created for the job (also the command's working directory). | Always. |
|
|
685
|
+
| `RA_JOB_BRANCH` | The bookmark/branch repoactive uses for the job's output. | Always. |
|
|
686
|
+
| `RA_JOB_BASE_BRANCH` | The branch the job's MR targets (`base_branch`, or `trunk()` by default). | Always. |
|
|
687
|
+
| `RA_CONFIG_SOURCE_DIR` | The directory of the config file that defined the command. | The command came from a config file (not a `--set` override or a built-in default). |
|
|
688
|
+
| `RA_JOBS_DIR` | The directory a generator writes its `*.toml` job fragments into. | The command is a [generator](#generating-jobs-dynamically) (`emits_jobs`). |
|
|
689
|
+
|
|
690
|
+
#### `RA_JOB_NAME`
|
|
691
|
+
|
|
692
|
+
`RA_JOB_NAME` holds the name of the job the command belongs to. A command
|
|
693
|
+
shared by several jobs (for instance one set in `[job-defaults]` or emitted
|
|
694
|
+
by a generator) can read it to tell which job is running:
|
|
695
|
+
|
|
696
|
+
```toml
|
|
697
|
+
[job.lint]
|
|
698
|
+
command = "./run-check.sh $RA_JOB_NAME"
|
|
699
|
+
title = "run the shared check script for this job"
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
#### `RA_WORKSPACE_DIR`
|
|
703
|
+
|
|
704
|
+
A job's command runs in a temporary jj workspace, which is also its working
|
|
705
|
+
directory. `RA_WORKSPACE_DIR` names that directory explicitly, so a command
|
|
706
|
+
that changes directory can still find its way back:
|
|
707
|
+
|
|
708
|
+
```toml
|
|
709
|
+
[job.build]
|
|
710
|
+
command = "cd subdir && make && cp result $RA_WORKSPACE_DIR/subdir/"
|
|
711
|
+
title = "build in a subdirectory"
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
#### `RA_JOB_BRANCH`
|
|
715
|
+
|
|
716
|
+
`RA_JOB_BRANCH` holds the bookmark (git branch) repoactive uses for the
|
|
717
|
+
job's output, e.g. `repoactive/uv-lock-upgrade`. The command runs on a fresh
|
|
718
|
+
commit while that bookmark still points at the **previous** run's commit, so
|
|
719
|
+
a command can inspect what it produced last time - for example to build on
|
|
720
|
+
it or diff against it:
|
|
721
|
+
|
|
722
|
+
```toml
|
|
723
|
+
[job.notes]
|
|
724
|
+
command = "jj diff --from $RA_JOB_BRANCH --to @ > /tmp/delta || true; ./make-notes.sh"
|
|
725
|
+
title = "regenerate release notes from what changed since last run"
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
The bookmark may not exist yet: a job's first run, a run that produced no
|
|
729
|
+
diff, and a generator all leave it unset on the remote, so a command that
|
|
730
|
+
reads it should tolerate a missing ref.
|
|
731
|
+
|
|
732
|
+
#### `RA_JOB_BASE_BRANCH`
|
|
733
|
+
|
|
734
|
+
`RA_JOB_BASE_BRANCH` holds the branch the job's merge request targets - its
|
|
735
|
+
`base_branch`, or `trunk()` when that is unset. A command can use it to look
|
|
736
|
+
at only what changed relative to the target, for instance to lint or
|
|
737
|
+
reformat just the affected files:
|
|
738
|
+
|
|
739
|
+
```toml
|
|
740
|
+
[job.format]
|
|
741
|
+
command = "treefmt $(git diff --name-only $RA_JOB_BASE_BRANCH)"
|
|
742
|
+
title = "format files changed since the base branch"
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
This is the branch you _configured_ as the base. When a job stacks on
|
|
746
|
+
another via `depends_on`, its commit actually sits on top of that other
|
|
747
|
+
job's output, so `RA_JOB_BASE_BRANCH` names the ultimate target of the
|
|
748
|
+
stack, not the immediate parent.
|
|
749
|
+
|
|
750
|
+
#### `RA_CONFIG_SOURCE_DIR`
|
|
751
|
+
|
|
752
|
+
Because the command runs in that throwaway workspace rather than in the
|
|
753
|
+
directory its config lives in, a relative path cannot reach a helper file
|
|
754
|
+
kept beside the config. `RA_CONFIG_SOURCE_DIR` bridges that: it holds the
|
|
755
|
+
directory of the config file that defined the job's command:
|
|
756
|
+
|
|
757
|
+
- a job defined in `.repoactive.toml` gets the directory holding it (the
|
|
758
|
+
repo directory, by default);
|
|
759
|
+
- a job defined in `.repoactive.d/foo.toml` gets the `.repoactive.d`
|
|
760
|
+
directory.
|
|
761
|
+
|
|
762
|
+
```toml
|
|
763
|
+
[job.fixup]
|
|
764
|
+
command = "$RA_CONFIG_SOURCE_DIR/fixup.sh"
|
|
765
|
+
title = "run the fixup script kept beside the config"
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
The value is the config file's real location on disk, so it is the same
|
|
769
|
+
whether the file was discovered automatically or named with `-c`. A command
|
|
770
|
+
whose value came from a `--set` override has no config file, so
|
|
771
|
+
`RA_CONFIG_SOURCE_DIR` is unset for it.
|
|
772
|
+
|
|
661
773
|
### Overriding values on the command line
|
|
662
774
|
|
|
663
775
|
`--set NAME=VALUE`/`-s` tweaks individual config values without editing a
|
|
@@ -831,12 +943,12 @@ changed anything in the current run, the job is skipped.
|
|
|
831
943
|
|
|
832
944
|
```toml
|
|
833
945
|
[job.prek-autoupdate]
|
|
834
|
-
command = "
|
|
946
|
+
command = "uvx prek autoupdate"
|
|
835
947
|
title = "ci: update pre-commit hooks"
|
|
836
948
|
tags = ["weekly"]
|
|
837
949
|
|
|
838
950
|
[job.prek-run-all]
|
|
839
|
-
command = "sh -c '
|
|
951
|
+
command = "sh -c 'uvx prek run -a; true'"
|
|
840
952
|
title = "ci: apply pre-commit fixes"
|
|
841
953
|
depends_on = ["prek-autoupdate"]
|
|
842
954
|
run_only_if_changed = ["prek-autoupdate"]
|
|
@@ -962,8 +1074,8 @@ job per package in a monorepo, one per entry in a manifest - and you don't
|
|
|
962
1074
|
want to hand-maintain them. A **generator** is an ordinary `[job.<name>]`
|
|
963
1075
|
with `emits_jobs = true`. Instead of producing a diff, its command writes
|
|
964
1076
|
one or more `*.toml` job fragments into the directory named by the
|
|
965
|
-
`
|
|
966
|
-
|
|
1077
|
+
`RA_JOBS_DIR` environment variable, and the jobs it emits run in the same
|
|
1078
|
+
invocation.
|
|
967
1079
|
|
|
968
1080
|
```toml
|
|
969
1081
|
[job.per-package]
|
|
@@ -978,7 +1090,7 @@ cooldown_period = "7d"
|
|
|
978
1090
|
The command writes fragments using the normal `[job.<name>]` syntax, e.g.
|
|
979
1091
|
|
|
980
1092
|
```toml
|
|
981
|
-
# $
|
|
1093
|
+
# $RA_JOBS_DIR/pkg-a.toml
|
|
982
1094
|
[job.upgrade-pkg-a]
|
|
983
1095
|
command = "uv lock --upgrade --package pkg-a"
|
|
984
1096
|
title = "build: upgrade pkg-a"
|
|
@@ -28,6 +28,7 @@ and (with `--mode publish`) the full MR lifecycle.
|
|
|
28
28
|
- [Example](#example)
|
|
29
29
|
- [`[platform.<name>]`](#platformname)
|
|
30
30
|
- [Config file locations](#config-file-locations)
|
|
31
|
+
- [Variables passed to job commands](#variables-passed-to-job-commands)
|
|
31
32
|
- [Overriding values on the command line](#overriding-values-on-the-command-line)
|
|
32
33
|
- [Selecting jobs with tags](#selecting-jobs-with-tags)
|
|
33
34
|
- [Keeping unmerged branches current](#keeping-unmerged-branches-current)
|
|
@@ -440,13 +441,17 @@ block.
|
|
|
440
441
|
**Branch and commit options:**
|
|
441
442
|
|
|
442
443
|
- **`branch_prefix`** (default: `"repoactive/"`) - Prefix prepended to the
|
|
443
|
-
job name to form the branch name.
|
|
444
|
+
job name to form the branch name. Set to `""` to use the job name alone.
|
|
445
|
+
With an empty prefix the branch name is exactly the job name, so make sure
|
|
446
|
+
no job is named after your base branch (e.g. a job named `main`) - the two
|
|
447
|
+
branches would otherwise collide.
|
|
444
448
|
- **`mr_title_prefix`** (default: `"[repoactive] "`) - Prefix prepended to
|
|
445
449
|
every MR/PR title. Set to `""` to disable.
|
|
446
450
|
- **`commit_title_prefix`** (default: `"[repoactive] "`) - Prefix prepended
|
|
447
451
|
to every commit title. Set to `""` to disable.
|
|
448
452
|
- **`base_branch`** (default: repo default) - Target branch for all jobs
|
|
449
|
-
that do not set their own.
|
|
453
|
+
that do not set their own. May also be a jj revset expression such as
|
|
454
|
+
`trunk()`, `root()`, or a user-defined revset alias.
|
|
450
455
|
|
|
451
456
|
**Run control:**
|
|
452
457
|
|
|
@@ -457,6 +462,10 @@ block.
|
|
|
457
462
|
- **`timeout`** (default: `"2m"`) - Maximum runtime for a job's command.
|
|
458
463
|
Same format as `cooldown_period`. See
|
|
459
464
|
[Limiting job runtime with `timeout`](#limiting-job-runtime-with-timeout).
|
|
465
|
+
- **`shell`** (default: `/bin/sh`) - Interpreter used to run job commands. A
|
|
466
|
+
bare name is resolved on `PATH` (e.g. `"bash"`); an absolute path also
|
|
467
|
+
works. The command runs as `<shell> -c <command>`. Arguments are not
|
|
468
|
+
allowed - the value must be a single binary.
|
|
460
469
|
|
|
461
470
|
### `[job.<name>]`
|
|
462
471
|
|
|
@@ -491,7 +500,8 @@ underscores.
|
|
|
491
500
|
**Branch and commit options:**
|
|
492
501
|
|
|
493
502
|
- **`base_branch`** (default: inherited) - Target branch for this job's
|
|
494
|
-
MR/PR.
|
|
503
|
+
MR/PR. May also be a jj revset expression such as `trunk()`, `root()`, or
|
|
504
|
+
a user-defined revset alias.
|
|
495
505
|
- **`branch_prefix`** (default: inherited) - Override the branch-name prefix
|
|
496
506
|
for this job only.
|
|
497
507
|
- **`mr_title_prefix`** (default: inherited) - Override the MR/PR title
|
|
@@ -526,9 +536,12 @@ underscores.
|
|
|
526
536
|
- **`timeout`** (default: inherited) - Maximum runtime for this job's
|
|
527
537
|
command. Set to `"0s"` to disable the timeout entirely. See
|
|
528
538
|
[Limiting job runtime with `timeout`](#limiting-job-runtime-with-timeout).
|
|
539
|
+
- **`shell`** (default: inherited) - Interpreter used to run this job's
|
|
540
|
+
command, overriding `job-defaults.shell`. See
|
|
541
|
+
[`job-defaults.shell`](#job-defaults).
|
|
529
542
|
- **`emits_jobs`** (default: `false`) - Generator job: the command writes
|
|
530
|
-
`*.toml` job fragments into `$
|
|
531
|
-
|
|
543
|
+
`*.toml` job fragments into `$RA_JOBS_DIR` instead of producing a diff.
|
|
544
|
+
See [Generating jobs dynamically](#generating-jobs-dynamically).
|
|
532
545
|
|
|
533
546
|
### Stacking MRs
|
|
534
547
|
|
|
@@ -631,6 +644,105 @@ directory of `*.toml` files, and may be repeated to merge several sources;
|
|
|
631
644
|
later sources win. Explicit paths are resolved relative to the current
|
|
632
645
|
directory, not `--repo`.
|
|
633
646
|
|
|
647
|
+
### Variables passed to job commands
|
|
648
|
+
|
|
649
|
+
`repoactive` injects a few `RA_`-prefixed environment variables into every
|
|
650
|
+
job command's environment. These are distinct from the `REPOACTIVE_`
|
|
651
|
+
variables [above](#environment-variables), which configure `repoactive`
|
|
652
|
+
itself.
|
|
653
|
+
|
|
654
|
+
| Variable | Value | Set when |
|
|
655
|
+
| ---------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
656
|
+
| `RA_JOB_NAME` | The name of the job the command belongs to. | Always. |
|
|
657
|
+
| `RA_WORKSPACE_DIR` | The temporary jj workspace created for the job (also the command's working directory). | Always. |
|
|
658
|
+
| `RA_JOB_BRANCH` | The bookmark/branch repoactive uses for the job's output. | Always. |
|
|
659
|
+
| `RA_JOB_BASE_BRANCH` | The branch the job's MR targets (`base_branch`, or `trunk()` by default). | Always. |
|
|
660
|
+
| `RA_CONFIG_SOURCE_DIR` | The directory of the config file that defined the command. | The command came from a config file (not a `--set` override or a built-in default). |
|
|
661
|
+
| `RA_JOBS_DIR` | The directory a generator writes its `*.toml` job fragments into. | The command is a [generator](#generating-jobs-dynamically) (`emits_jobs`). |
|
|
662
|
+
|
|
663
|
+
#### `RA_JOB_NAME`
|
|
664
|
+
|
|
665
|
+
`RA_JOB_NAME` holds the name of the job the command belongs to. A command
|
|
666
|
+
shared by several jobs (for instance one set in `[job-defaults]` or emitted
|
|
667
|
+
by a generator) can read it to tell which job is running:
|
|
668
|
+
|
|
669
|
+
```toml
|
|
670
|
+
[job.lint]
|
|
671
|
+
command = "./run-check.sh $RA_JOB_NAME"
|
|
672
|
+
title = "run the shared check script for this job"
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
#### `RA_WORKSPACE_DIR`
|
|
676
|
+
|
|
677
|
+
A job's command runs in a temporary jj workspace, which is also its working
|
|
678
|
+
directory. `RA_WORKSPACE_DIR` names that directory explicitly, so a command
|
|
679
|
+
that changes directory can still find its way back:
|
|
680
|
+
|
|
681
|
+
```toml
|
|
682
|
+
[job.build]
|
|
683
|
+
command = "cd subdir && make && cp result $RA_WORKSPACE_DIR/subdir/"
|
|
684
|
+
title = "build in a subdirectory"
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
#### `RA_JOB_BRANCH`
|
|
688
|
+
|
|
689
|
+
`RA_JOB_BRANCH` holds the bookmark (git branch) repoactive uses for the
|
|
690
|
+
job's output, e.g. `repoactive/uv-lock-upgrade`. The command runs on a fresh
|
|
691
|
+
commit while that bookmark still points at the **previous** run's commit, so
|
|
692
|
+
a command can inspect what it produced last time - for example to build on
|
|
693
|
+
it or diff against it:
|
|
694
|
+
|
|
695
|
+
```toml
|
|
696
|
+
[job.notes]
|
|
697
|
+
command = "jj diff --from $RA_JOB_BRANCH --to @ > /tmp/delta || true; ./make-notes.sh"
|
|
698
|
+
title = "regenerate release notes from what changed since last run"
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
The bookmark may not exist yet: a job's first run, a run that produced no
|
|
702
|
+
diff, and a generator all leave it unset on the remote, so a command that
|
|
703
|
+
reads it should tolerate a missing ref.
|
|
704
|
+
|
|
705
|
+
#### `RA_JOB_BASE_BRANCH`
|
|
706
|
+
|
|
707
|
+
`RA_JOB_BASE_BRANCH` holds the branch the job's merge request targets - its
|
|
708
|
+
`base_branch`, or `trunk()` when that is unset. A command can use it to look
|
|
709
|
+
at only what changed relative to the target, for instance to lint or
|
|
710
|
+
reformat just the affected files:
|
|
711
|
+
|
|
712
|
+
```toml
|
|
713
|
+
[job.format]
|
|
714
|
+
command = "treefmt $(git diff --name-only $RA_JOB_BASE_BRANCH)"
|
|
715
|
+
title = "format files changed since the base branch"
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
This is the branch you _configured_ as the base. When a job stacks on
|
|
719
|
+
another via `depends_on`, its commit actually sits on top of that other
|
|
720
|
+
job's output, so `RA_JOB_BASE_BRANCH` names the ultimate target of the
|
|
721
|
+
stack, not the immediate parent.
|
|
722
|
+
|
|
723
|
+
#### `RA_CONFIG_SOURCE_DIR`
|
|
724
|
+
|
|
725
|
+
Because the command runs in that throwaway workspace rather than in the
|
|
726
|
+
directory its config lives in, a relative path cannot reach a helper file
|
|
727
|
+
kept beside the config. `RA_CONFIG_SOURCE_DIR` bridges that: it holds the
|
|
728
|
+
directory of the config file that defined the job's command:
|
|
729
|
+
|
|
730
|
+
- a job defined in `.repoactive.toml` gets the directory holding it (the
|
|
731
|
+
repo directory, by default);
|
|
732
|
+
- a job defined in `.repoactive.d/foo.toml` gets the `.repoactive.d`
|
|
733
|
+
directory.
|
|
734
|
+
|
|
735
|
+
```toml
|
|
736
|
+
[job.fixup]
|
|
737
|
+
command = "$RA_CONFIG_SOURCE_DIR/fixup.sh"
|
|
738
|
+
title = "run the fixup script kept beside the config"
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
The value is the config file's real location on disk, so it is the same
|
|
742
|
+
whether the file was discovered automatically or named with `-c`. A command
|
|
743
|
+
whose value came from a `--set` override has no config file, so
|
|
744
|
+
`RA_CONFIG_SOURCE_DIR` is unset for it.
|
|
745
|
+
|
|
634
746
|
### Overriding values on the command line
|
|
635
747
|
|
|
636
748
|
`--set NAME=VALUE`/`-s` tweaks individual config values without editing a
|
|
@@ -804,12 +916,12 @@ changed anything in the current run, the job is skipped.
|
|
|
804
916
|
|
|
805
917
|
```toml
|
|
806
918
|
[job.prek-autoupdate]
|
|
807
|
-
command = "
|
|
919
|
+
command = "uvx prek autoupdate"
|
|
808
920
|
title = "ci: update pre-commit hooks"
|
|
809
921
|
tags = ["weekly"]
|
|
810
922
|
|
|
811
923
|
[job.prek-run-all]
|
|
812
|
-
command = "sh -c '
|
|
924
|
+
command = "sh -c 'uvx prek run -a; true'"
|
|
813
925
|
title = "ci: apply pre-commit fixes"
|
|
814
926
|
depends_on = ["prek-autoupdate"]
|
|
815
927
|
run_only_if_changed = ["prek-autoupdate"]
|
|
@@ -935,8 +1047,8 @@ job per package in a monorepo, one per entry in a manifest - and you don't
|
|
|
935
1047
|
want to hand-maintain them. A **generator** is an ordinary `[job.<name>]`
|
|
936
1048
|
with `emits_jobs = true`. Instead of producing a diff, its command writes
|
|
937
1049
|
one or more `*.toml` job fragments into the directory named by the
|
|
938
|
-
`
|
|
939
|
-
|
|
1050
|
+
`RA_JOBS_DIR` environment variable, and the jobs it emits run in the same
|
|
1051
|
+
invocation.
|
|
940
1052
|
|
|
941
1053
|
```toml
|
|
942
1054
|
[job.per-package]
|
|
@@ -951,7 +1063,7 @@ cooldown_period = "7d"
|
|
|
951
1063
|
The command writes fragments using the normal `[job.<name>]` syntax, e.g.
|
|
952
1064
|
|
|
953
1065
|
```toml
|
|
954
|
-
# $
|
|
1066
|
+
# $RA_JOBS_DIR/pkg-a.toml
|
|
955
1067
|
[job.upgrade-pkg-a]
|
|
956
1068
|
command = "uv lock --upgrade --package pkg-a"
|
|
957
1069
|
title = "build: upgrade pkg-a"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Render and strip boxquote.el-style boxes used to embed command output in messages."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
# A boxquote block: a ``,----`` opening line through its closing ``` `---- ``` line.
|
|
6
|
+
# DOTALL lets ``.*?`` span the body lines; MULTILINE anchors both delimiters to a
|
|
7
|
+
# line of their own. Non-greedy so each block stops at its own closing line.
|
|
8
|
+
_BOXQUOTE_BLOCK = re.compile(r"^,----.*?^`----[ \t]*$", re.MULTILINE | re.DOTALL)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def boxquote(msg: str, title: str = "") -> str:
|
|
12
|
+
"""Render ``msg`` inside a boxquote.el-style box.
|
|
13
|
+
|
|
14
|
+
The box opens with ``,----[ title ]`` (or just ``,----`` when ``title`` is
|
|
15
|
+
empty), each line of ``msg`` is prefixed with ``| ``, and the box closes
|
|
16
|
+
with ``` `---- ```.
|
|
17
|
+
|
|
18
|
+
A multi-line ``title`` (e.g. a multi-line job command) is bracketed across
|
|
19
|
+
several lines instead of jammed onto the opening line: the first title line
|
|
20
|
+
follows ``,----[ ``, the remaining lines are ``|``-prefixed and indented to
|
|
21
|
+
align under that first line, the closing ``]`` trails the last of them, and
|
|
22
|
+
a blank ``|`` line then separates the bracketed title from ``msg``. A
|
|
23
|
+
trailing newline on ``title`` is dropped so it does not produce an empty
|
|
24
|
+
final title line.
|
|
25
|
+
"""
|
|
26
|
+
body = "\n".join(f"| {line}" for line in msg.splitlines())
|
|
27
|
+
title = title.rstrip("\n")
|
|
28
|
+
if "\n" not in title:
|
|
29
|
+
top = f",----[ {title} ]" if title else ",----"
|
|
30
|
+
return f"{top}\n{body}\n`----"
|
|
31
|
+
open_bracket = ",----[ "
|
|
32
|
+
# Indent continuation lines so the title text aligns under the first line;
|
|
33
|
+
# the "|" replaces the leading "," so the bar still runs down the left edge.
|
|
34
|
+
indent = "|" + " " * (len(open_bracket) - 1)
|
|
35
|
+
head, *rest = title.split("\n")
|
|
36
|
+
header_lines = [f"{open_bracket}{head}", *(f"{indent}{line}" for line in rest[:-1])]
|
|
37
|
+
header_lines.append(f"{indent}{rest[-1]} ]")
|
|
38
|
+
header = "\n".join(header_lines)
|
|
39
|
+
return f"{header}\n|\n{body}\n`----"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def strip_boxquotes(text: str) -> str:
|
|
43
|
+
"""Remove every boxquote block (see :func:`boxquote`) from ``text``.
|
|
44
|
+
|
|
45
|
+
A block runs from a ``,----`` opening line to its closing ``` `---- ``` line;
|
|
46
|
+
the surrounding text is kept. Blank lines exposed by a removed block are
|
|
47
|
+
collapsed so paragraphs stay separated by a single blank line, and leading
|
|
48
|
+
and trailing blank lines are trimmed.
|
|
49
|
+
"""
|
|
50
|
+
without = _BOXQUOTE_BLOCK.sub("", text)
|
|
51
|
+
return re.sub(r"\n{3,}", "\n\n", without).strip("\n")
|
|
@@ -33,7 +33,8 @@ logger = logging.getLogger(__name__)
|
|
|
33
33
|
_DURATION_RE = re.compile(r"^(\d+)([smhdw])$")
|
|
34
34
|
_JOB_NAME_RE = re.compile(r"^[A-Za-z0-9_-]+$")
|
|
35
35
|
_TAG_RE = re.compile(r"^[A-Za-z0-9_-]+$")
|
|
36
|
-
|
|
36
|
+
# An empty prefix is allowed (branch name == job name); hence * not +.
|
|
37
|
+
_BRANCH_PREFIX_RE = re.compile(r"^(?!/)(?!.*//)[a-zA-Z0-9_\-/]*$")
|
|
37
38
|
_DURATION_UNITS = {"s": "seconds", "m": "minutes", "h": "hours", "d": "days", "w": "weeks"}
|
|
38
39
|
|
|
39
40
|
# Reserved tags driving job selection. A plain job carries DEFAULT_TAG, which is
|
|
@@ -63,6 +64,17 @@ class InvalidBranchPrefixError(ValueError):
|
|
|
63
64
|
)
|
|
64
65
|
|
|
65
66
|
|
|
67
|
+
class InvalidShellError(ValueError):
|
|
68
|
+
"""Raised when a shell value is not a single interpreter binary."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, value: str) -> None:
|
|
71
|
+
super().__init__(
|
|
72
|
+
f"invalid shell {value!r}: must be a single interpreter binary "
|
|
73
|
+
"(a name resolved on PATH like 'bash', or an absolute path); "
|
|
74
|
+
"arguments are not allowed"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
66
78
|
class InvalidJobNameError(ValueError):
|
|
67
79
|
"""Raised when a job name contains disallowed characters."""
|
|
68
80
|
|
|
@@ -157,6 +169,20 @@ class GeneratedByInBodyError(ValueError):
|
|
|
157
169
|
)
|
|
158
170
|
|
|
159
171
|
|
|
172
|
+
class ConfigSourceDirInBodyError(ValueError):
|
|
173
|
+
"""Raised when a [job.<name>] table sets a 'config_source_dir' field.
|
|
174
|
+
|
|
175
|
+
'config_source_dir' is set by repoactive to the directory of the config
|
|
176
|
+
source that defined the job's command; it is not a user-facing config field.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def __init__(self, name: str) -> None:
|
|
180
|
+
super().__init__(
|
|
181
|
+
f"job {name!r} must not set 'config_source_dir'; "
|
|
182
|
+
"that field is set by repoactive to the job's config directory"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
160
186
|
class DuplicatePlatformHostError(ValueError):
|
|
161
187
|
"""Raised when two platforms resolve to the same host.
|
|
162
188
|
|
|
@@ -235,6 +261,16 @@ def _validate_duration(value: str) -> str:
|
|
|
235
261
|
return value
|
|
236
262
|
|
|
237
263
|
|
|
264
|
+
def _validate_shell(value: str) -> str:
|
|
265
|
+
# The value maps to subprocess' executable= (run as `<shell> -c <command>`),
|
|
266
|
+
# so it must be a single binary, not an argv with flags. Reject empty and any
|
|
267
|
+
# whitespace so a "full argv" like "bash -e" fails at config load instead of
|
|
268
|
+
# silently looking up a binary literally named "bash -e".
|
|
269
|
+
if not value or any(c.isspace() for c in value):
|
|
270
|
+
raise InvalidShellError(value)
|
|
271
|
+
return value
|
|
272
|
+
|
|
273
|
+
|
|
238
274
|
def _validate_single_line(value: str, info: ValidationInfo) -> str:
|
|
239
275
|
if "\n" in value:
|
|
240
276
|
assert info.field_name is not None
|
|
@@ -244,6 +280,7 @@ def _validate_single_line(value: str, info: ValidationInfo) -> str:
|
|
|
244
280
|
|
|
245
281
|
_BranchPrefix = Annotated[str, AfterValidator(_validate_branch_prefix)]
|
|
246
282
|
_Duration = Annotated[str, AfterValidator(_validate_duration)]
|
|
283
|
+
_Shell = Annotated[str, AfterValidator(_validate_shell)]
|
|
247
284
|
# Titles and title prefixes end up in commit subjects and MR titles, which are
|
|
248
285
|
# single-line by nature.
|
|
249
286
|
_SingleLine = Annotated[str, AfterValidator(_validate_single_line)]
|
|
@@ -265,6 +302,10 @@ class JobDefaults(BaseModel):
|
|
|
265
302
|
timeout: _Duration | None = "2m"
|
|
266
303
|
auto_merge: bool = False
|
|
267
304
|
required_approvals: int | None = Field(default=None, ge=0)
|
|
305
|
+
# Interpreter for job commands, passed to subprocess as executable= and run as
|
|
306
|
+
# `<shell> -c <command>`. None means /bin/sh (the shell=True default). May be a
|
|
307
|
+
# bare name resolved on PATH (e.g. "bash") or an absolute path.
|
|
308
|
+
shell: _Shell | None = None
|
|
268
309
|
|
|
269
310
|
|
|
270
311
|
# Fields Job.resolve fills in from JobDefaults when the job does not set them
|
|
@@ -278,6 +319,7 @@ _DEFAULTED_FIELDS = (
|
|
|
278
319
|
"timeout",
|
|
279
320
|
"auto_merge",
|
|
280
321
|
"required_approvals",
|
|
322
|
+
"shell",
|
|
281
323
|
)
|
|
282
324
|
|
|
283
325
|
|
|
@@ -303,13 +345,18 @@ class Job(BaseModel):
|
|
|
303
345
|
output_in_commit: bool = True
|
|
304
346
|
# When true, the command does not produce a diff to commit; instead it writes
|
|
305
347
|
# one or more *.toml job fragments into the directory named by the
|
|
306
|
-
#
|
|
348
|
+
# RA_JOBS_DIR environment variable, and those jobs are run in the same
|
|
307
349
|
# invocation. See docs/adr/0004-job-generators.md.
|
|
308
350
|
emits_jobs: bool = False
|
|
309
351
|
# Set by repoactive (never written in config) on jobs produced by a generator:
|
|
310
352
|
# the generator's name, recorded as a second Repoactive-Job trailer so the
|
|
311
353
|
# generator gets a meaningful cooldown over the whole fan-out.
|
|
312
354
|
generated_by: str | None = None
|
|
355
|
+
# Set by repoactive (never written in config): the absolute directory of the
|
|
356
|
+
# config source that defined this job's command, exported to the command as
|
|
357
|
+
# RA_CONFIG_SOURCE_DIR so it can reach files kept beside its config. None when
|
|
358
|
+
# the command came from a --set override or the built-in defaults.
|
|
359
|
+
config_source_dir: str | None = None
|
|
313
360
|
|
|
314
361
|
# the following fields will be resolved from the defaults
|
|
315
362
|
branch_prefix: _BranchPrefix | None = None
|
|
@@ -320,6 +367,7 @@ class Job(BaseModel):
|
|
|
320
367
|
timeout: _Duration | None = None
|
|
321
368
|
auto_merge: bool | None = None
|
|
322
369
|
required_approvals: int | None = Field(default=None, ge=0)
|
|
370
|
+
shell: _Shell | None = None
|
|
323
371
|
|
|
324
372
|
@field_validator("name")
|
|
325
373
|
@classmethod
|
|
@@ -432,6 +480,8 @@ class Config(BaseModel):
|
|
|
432
480
|
raise JobNameInBodyError(name)
|
|
433
481
|
if "generated_by" in body:
|
|
434
482
|
raise GeneratedByInBodyError(name)
|
|
483
|
+
if "config_source_dir" in body:
|
|
484
|
+
raise ConfigSourceDirInBodyError(name)
|
|
435
485
|
jobs.append({**body, "name": name})
|
|
436
486
|
return jobs
|
|
437
487
|
|
|
@@ -517,8 +567,18 @@ class Config(BaseModel):
|
|
|
517
567
|
return {job.branch_name() for job in self._resolved_jobs()}
|
|
518
568
|
|
|
519
569
|
def base_branches(self) -> set[str]:
|
|
520
|
-
"""
|
|
521
|
-
|
|
570
|
+
"""Bookmark names a job uses as base_branch.
|
|
571
|
+
|
|
572
|
+
These are the branches to track with ``jj bookmark track``. Values that
|
|
573
|
+
look like a revset function call (``trunk()``, ``root()``, or a
|
|
574
|
+
user-defined revset alias) are excluded: they are valid base branches but
|
|
575
|
+
not bookmarks, so tracking them would fail.
|
|
576
|
+
"""
|
|
577
|
+
return {
|
|
578
|
+
job.base_branch
|
|
579
|
+
for job in self._resolved_jobs()
|
|
580
|
+
if job.base_branch and "()" not in job.base_branch
|
|
581
|
+
}
|
|
522
582
|
|
|
523
583
|
def token_env_names(self) -> set[str]:
|
|
524
584
|
"""Names of the env vars holding platform API tokens.
|
|
@@ -657,6 +717,10 @@ class _ConfigSource:
|
|
|
657
717
|
|
|
658
718
|
label: str
|
|
659
719
|
data: dict
|
|
720
|
+
# Absolute directory of the file this source was read from, or None for
|
|
721
|
+
# sources without a file (built-in defaults, --set overrides). Exposed to a
|
|
722
|
+
# command as RA_CONFIG_SOURCE_DIR for the job whose command this source sets.
|
|
723
|
+
source_dir: Path | None = None
|
|
660
724
|
|
|
661
725
|
|
|
662
726
|
def _built_in_defaults() -> list[_ConfigSource]:
|
|
@@ -693,7 +757,9 @@ def _parse_override(text: str) -> _ConfigSource:
|
|
|
693
757
|
|
|
694
758
|
def _read_toml_file(path: Path) -> _ConfigSource:
|
|
695
759
|
try:
|
|
696
|
-
return _ConfigSource(
|
|
760
|
+
return _ConfigSource(
|
|
761
|
+
str(path), tomllib.loads(path.read_text()), source_dir=path.resolve().parent
|
|
762
|
+
)
|
|
697
763
|
except (OSError, tomllib.TOMLDecodeError) as e:
|
|
698
764
|
raise ConfigError(str(path), e) from e
|
|
699
765
|
|
|
@@ -710,12 +776,19 @@ def load_config(paths: list[Path], overrides: list[str] | None = None) -> Config
|
|
|
710
776
|
|
|
711
777
|
def _merge_config(sources: Iterable[_ConfigSource]) -> Config:
|
|
712
778
|
merged = {}
|
|
779
|
+
# Directory of the config source that last set each job's command, keyed by
|
|
780
|
+
# job name. Exported to the command as RA_CONFIG_SOURCE_DIR (see
|
|
781
|
+
# Job.config_source_dir). None when that source has no file (--set, built-in).
|
|
782
|
+
command_source_dir: dict[str, Path | None] = {}
|
|
713
783
|
for source in sources:
|
|
714
784
|
data = source.data
|
|
715
785
|
try:
|
|
716
786
|
# Reject odd shapes (e.g. job.foo = "hello") up front; the merge
|
|
717
787
|
# helpers assume tables of tables and would fail cryptically.
|
|
718
788
|
shape = ConfigShape.model_validate(data)
|
|
789
|
+
for name, body in shape.job.items():
|
|
790
|
+
if "command" in body:
|
|
791
|
+
command_source_dir[name] = source.source_dir
|
|
719
792
|
jobs = merge_jobs(base=merged.get("job", {}), override=shape.job)
|
|
720
793
|
platforms = merge_platforms(
|
|
721
794
|
base=merged.get("platform", {}),
|
|
@@ -732,5 +805,18 @@ def _merge_config(sources: Iterable[_ConfigSource]) -> Config:
|
|
|
732
805
|
except (ValueError, ValidationError) as e:
|
|
733
806
|
raise ConfigError(source.label, e) from e
|
|
734
807
|
config = Config.model_validate(merged)
|
|
808
|
+
# Stamp each job with the directory of the source that defined its command.
|
|
809
|
+
# Done after validation (not injected into the raw table) so it bypasses the
|
|
810
|
+
# ConfigSourceDirInBodyError guard, which only rejects user-written values.
|
|
811
|
+
config = config.model_copy(
|
|
812
|
+
update={
|
|
813
|
+
"jobs": [
|
|
814
|
+
job.model_copy(update={"config_source_dir": str(command_source_dir[job.name])})
|
|
815
|
+
if command_source_dir.get(job.name) is not None
|
|
816
|
+
else job
|
|
817
|
+
for job in config.jobs
|
|
818
|
+
]
|
|
819
|
+
}
|
|
820
|
+
)
|
|
735
821
|
logger.debug("loaded config: %s", config.model_dump_json(indent=2))
|
|
736
822
|
return config
|
|
@@ -49,7 +49,37 @@ logger = logging.getLogger(__name__)
|
|
|
49
49
|
|
|
50
50
|
# Environment variable naming the directory a generator (``emits_jobs``) command
|
|
51
51
|
# writes its ``*.toml`` job fragments into. See docs/adr/0004-job-generators.md.
|
|
52
|
-
|
|
52
|
+
RA_JOBS_DIR_ENV = "RA_JOBS_DIR"
|
|
53
|
+
|
|
54
|
+
# Environment variable exposing to a job command the directory of the config
|
|
55
|
+
# source that defined the command (Job.config_source_dir), so the command can
|
|
56
|
+
# reach files kept beside its config. See docs/adr/0016-injected-env-var-prefix.md.
|
|
57
|
+
RA_CONFIG_SOURCE_DIR_ENV = "RA_CONFIG_SOURCE_DIR"
|
|
58
|
+
|
|
59
|
+
# Environment variable exposing to a job command the throwaway jj workspace
|
|
60
|
+
# repoactive created for it. This is always the command's working directory, but
|
|
61
|
+
# naming it explicitly lets a command that changes directory find its way back.
|
|
62
|
+
RA_WORKSPACE_DIR_ENV = "RA_WORKSPACE_DIR"
|
|
63
|
+
|
|
64
|
+
# Environment variable exposing to a job command the bookmark/branch repoactive
|
|
65
|
+
# uses for the job's output (Job.branch_name). The command runs on a fresh commit
|
|
66
|
+
# while this bookmark still points at the previous run's commit, so the command
|
|
67
|
+
# can inspect what it produced last time (e.g. `git diff $RA_JOB_BRANCH`). The
|
|
68
|
+
# bookmark may not exist yet: a first run, a run that produced no diff, or a
|
|
69
|
+
# generator never creates it.
|
|
70
|
+
RA_JOB_BRANCH_ENV = "RA_JOB_BRANCH"
|
|
71
|
+
|
|
72
|
+
# Environment variable exposing to a job command the name of the job it belongs
|
|
73
|
+
# to (Job.name), so a command shared by several jobs can tell which one is
|
|
74
|
+
# running (e.g. to label its output).
|
|
75
|
+
RA_JOB_NAME_ENV = "RA_JOB_NAME"
|
|
76
|
+
|
|
77
|
+
# Environment variable exposing to a job command the branch the job's MR targets
|
|
78
|
+
# (Job.base_branch, or "trunk()" by default), so a command can diff against its
|
|
79
|
+
# target (e.g. `git diff $RA_JOB_BASE_BRANCH`). This is the *configured* base; for a
|
|
80
|
+
# stacked job (depends_on) the immediate parent commit is another job's output,
|
|
81
|
+
# not this value.
|
|
82
|
+
RA_JOB_BASE_BRANCH_ENV = "RA_JOB_BASE_BRANCH"
|
|
53
83
|
|
|
54
84
|
# Fields an emitted job inherits from its generator when the emitted entry does
|
|
55
85
|
# not set them itself (``tags`` and ``depends_on`` are handled separately because
|
|
@@ -245,7 +275,7 @@ def _command_env(
|
|
|
245
275
|
Starts from the inherited environment (so the command still sees PATH etc.),
|
|
246
276
|
drops the platform token variables (``secret_env_names``) so a command cannot read
|
|
247
277
|
the credential repoactive uses to push/create MRs, then layers on ``extra_env``
|
|
248
|
-
(e.g.
|
|
278
|
+
(e.g. RA_JOBS_DIR for a generator). See
|
|
249
279
|
docs/adr/0006-job-commands-are-trusted.md.
|
|
250
280
|
"""
|
|
251
281
|
env = {k: v for k, v in os.environ.items() if k not in secret_env_names}
|
|
@@ -254,6 +284,25 @@ def _command_env(
|
|
|
254
284
|
return env
|
|
255
285
|
|
|
256
286
|
|
|
287
|
+
def _job_extra_env(job: Job, extra: dict[str, str] | None = None) -> dict[str, str]:
|
|
288
|
+
"""Extra environment for ``job``'s command: its name, branches, config dir, ``extra``.
|
|
289
|
+
|
|
290
|
+
Always adds RA_JOB_NAME (the job's name), RA_JOB_BRANCH (the bookmark
|
|
291
|
+
repoactive uses for the job's output), and RA_JOB_BASE_BRANCH (the branch the
|
|
292
|
+
job's MR targets). Adds RA_CONFIG_SOURCE_DIR when the job has a
|
|
293
|
+
``config_source_dir`` (the directory of the config source that defined its
|
|
294
|
+
command), on top of any caller-supplied entries (e.g. RA_JOBS_DIR for a
|
|
295
|
+
generator).
|
|
296
|
+
"""
|
|
297
|
+
env = dict(extra or {})
|
|
298
|
+
env[RA_JOB_NAME_ENV] = job.name
|
|
299
|
+
env[RA_JOB_BRANCH_ENV] = job.branch_name()
|
|
300
|
+
env[RA_JOB_BASE_BRANCH_ENV] = job.base_branch or "trunk()"
|
|
301
|
+
if job.config_source_dir is not None:
|
|
302
|
+
env[RA_CONFIG_SOURCE_DIR_ENV] = job.config_source_dir
|
|
303
|
+
return env
|
|
304
|
+
|
|
305
|
+
|
|
257
306
|
@contextlib.contextmanager
|
|
258
307
|
def _watchdog(proc: subprocess.Popen[str], timeout: float | None) -> Generator[threading.Event]:
|
|
259
308
|
"""Kill ``proc``'s process group if it outlives ``timeout`` seconds.
|
|
@@ -298,6 +347,9 @@ def _spawn(job: Job, cwd: Path, env: dict[str, str]) -> Generator[subprocess.Pop
|
|
|
298
347
|
proc = subprocess.Popen(
|
|
299
348
|
job.command,
|
|
300
349
|
shell=True,
|
|
350
|
+
# None keeps subprocess' shell=True default of /bin/sh; a configured shell
|
|
351
|
+
# runs the command as `<shell> -c <command>` (see Job.shell).
|
|
352
|
+
executable=job.shell,
|
|
301
353
|
cwd=cwd,
|
|
302
354
|
stdout=subprocess.PIPE,
|
|
303
355
|
stderr=subprocess.STDOUT,
|
|
@@ -327,7 +379,12 @@ def _run_command(
|
|
|
327
379
|
extra_env: dict[str, str] | None = None,
|
|
328
380
|
) -> CommandResult:
|
|
329
381
|
start = time.monotonic()
|
|
330
|
-
|
|
382
|
+
# The workspace is always cwd, but expose it explicitly so a command that
|
|
383
|
+
# cd's elsewhere can still find the workspace repoactive prepared for it.
|
|
384
|
+
env = _command_env(
|
|
385
|
+
extra_env={**(extra_env or {}), RA_WORKSPACE_DIR_ENV: str(cwd)},
|
|
386
|
+
secret_env_names=secret_env_names,
|
|
387
|
+
)
|
|
331
388
|
|
|
332
389
|
# Stream the merged stdout/stderr line by line: keep the full output (needed
|
|
333
390
|
# for the commit message and the success result) while feeding a live tail of
|
|
@@ -498,7 +555,12 @@ def run_job(
|
|
|
498
555
|
repo.git_sync_head()
|
|
499
556
|
logger.debug("[%s] running command: %s", job.name, job.command)
|
|
500
557
|
try:
|
|
501
|
-
command_result = _run_command(
|
|
558
|
+
command_result = _run_command(
|
|
559
|
+
job,
|
|
560
|
+
repo.cwd,
|
|
561
|
+
secret_env_names=ctx.secret_env_names,
|
|
562
|
+
extra_env=_job_extra_env(job),
|
|
563
|
+
)
|
|
502
564
|
except CommandError:
|
|
503
565
|
# The command timed out or failed; discard only the fresh commit.
|
|
504
566
|
# The old bookmark is completely unaffected.
|
|
@@ -571,6 +633,9 @@ def _build_generated_job(
|
|
|
571
633
|
for f in _INHERITED_FIELDS:
|
|
572
634
|
merged.setdefault(f, getattr(generator, f))
|
|
573
635
|
merged["generated_by"] = generator.name
|
|
636
|
+
# The generator's fragments live in a throwaway temp dir, so an emitted job's
|
|
637
|
+
# meaningful config location is the generator's own config source.
|
|
638
|
+
merged["config_source_dir"] = generator.config_source_dir
|
|
574
639
|
try:
|
|
575
640
|
return Job.model_validate(merged)
|
|
576
641
|
except ValidationError as e:
|
|
@@ -785,7 +850,7 @@ def _run_generator_job(
|
|
|
785
850
|
"""Run a generator and return its emitted jobs (resolved ``job`` required).
|
|
786
851
|
|
|
787
852
|
The command runs in a fresh workspace on top of ``parents`` with
|
|
788
|
-
``
|
|
853
|
+
``RA_JOBS_DIR`` pointing at an empty directory; it writes ``*.toml``
|
|
789
854
|
fragments there which are parsed once it exits. The generator itself produces
|
|
790
855
|
no diff: any working-copy change it leaves is discarded (ADR 0004), and a
|
|
791
856
|
no-op ``JobResult`` is recorded so its emitted jobs (which depend on it)
|
|
@@ -808,7 +873,7 @@ def _run_generator_job(
|
|
|
808
873
|
job,
|
|
809
874
|
repo.cwd,
|
|
810
875
|
secret_env_names=ctx.secret_env_names,
|
|
811
|
-
extra_env={
|
|
876
|
+
extra_env=_job_extra_env(job, {RA_JOBS_DIR_ENV: str(jobs_dir)}),
|
|
812
877
|
)
|
|
813
878
|
specs = _load_job_specs(jobs_dir)
|
|
814
879
|
finally:
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
"""Render and strip boxquote.el-style boxes used to embed command output in messages."""
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
|
|
5
|
-
# A boxquote block: a ``,----`` opening line through its closing ``` `---- ``` line.
|
|
6
|
-
# DOTALL lets ``.*?`` span the body lines; MULTILINE anchors both delimiters to a
|
|
7
|
-
# line of their own. Non-greedy so each block stops at its own closing line.
|
|
8
|
-
_BOXQUOTE_BLOCK = re.compile(r"^,----.*?^`----[ \t]*$", re.MULTILINE | re.DOTALL)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def boxquote(msg: str, title: str = "") -> str:
|
|
12
|
-
"""Render ``msg`` inside a boxquote.el-style box.
|
|
13
|
-
|
|
14
|
-
The first line is ``,----[ title ]`` (or just ``,----`` when ``title`` is
|
|
15
|
-
empty), each line of ``msg`` is prefixed with ``| ``, and the box closes
|
|
16
|
-
with ``` `---- ```.
|
|
17
|
-
"""
|
|
18
|
-
top = f",----[ {title} ]" if title else ",----"
|
|
19
|
-
body = "\n".join(f"| {line}" for line in msg.splitlines())
|
|
20
|
-
return f"{top}\n{body}\n`----"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def strip_boxquotes(text: str) -> str:
|
|
24
|
-
"""Remove every boxquote block (see :func:`boxquote`) from ``text``.
|
|
25
|
-
|
|
26
|
-
A block runs from a ``,----`` opening line to its closing ``` `---- ``` line;
|
|
27
|
-
the surrounding text is kept. Blank lines exposed by a removed block are
|
|
28
|
-
collapsed so paragraphs stay separated by a single blank line, and leading
|
|
29
|
-
and trailing blank lines are trimmed.
|
|
30
|
-
"""
|
|
31
|
-
without = _BOXQUOTE_BLOCK.sub("", text)
|
|
32
|
-
return re.sub(r"\n{3,}", "\n\n", without).strip("\n")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|