pl-e2e-test 0.1.8__tar.gz → 0.2.1__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 (76) hide show
  1. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/MANIFEST.in +1 -0
  2. {pl_e2e_test-0.1.8/src/pl_e2e_test.egg-info → pl_e2e_test-0.2.1}/PKG-INFO +139 -69
  3. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/README.md +136 -68
  4. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/pyproject.toml +3 -1
  5. pl_e2e_test-0.2.1/scripts/demo_cli.py +219 -0
  6. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1/src/pl_e2e_test.egg-info}/PKG-INFO +139 -69
  7. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/pl_e2e_test.egg-info/SOURCES.txt +9 -0
  8. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/pl_e2e_test.egg-info/requires.txt +2 -0
  9. pl_e2e_test-0.2.1/src/prairielearn_e2e/__init__.py +50 -0
  10. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/prairielearn_e2e/_git.py +47 -3
  11. pl_e2e_test-0.2.1/src/prairielearn_e2e/_options.py +29 -0
  12. pl_e2e_test-0.2.1/src/prairielearn_e2e/cli.py +721 -0
  13. pl_e2e_test-0.2.1/src/prairielearn_e2e/config.py +150 -0
  14. pl_e2e_test-0.2.1/src/prairielearn_e2e/fuzz.py +462 -0
  15. pl_e2e_test-0.2.1/src/prairielearn_e2e/instances.py +199 -0
  16. pl_e2e_test-0.2.1/src/prairielearn_e2e/parallel.py +350 -0
  17. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/prairielearn_e2e/pytest_plugin.py +91 -2
  18. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/prairielearn_e2e/regression.py +22 -31
  19. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/prairielearn_e2e/reporting.py +401 -35
  20. pl_e2e_test-0.2.1/src/prairielearn_e2e/repository.py +668 -0
  21. pl_e2e_test-0.2.1/src/prairielearn_e2e/runner.py +82 -0
  22. pl_e2e_test-0.2.1/src/prairielearn_e2e/runtime.py +410 -0
  23. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/prairielearn_e2e/sharding.py +4 -5
  24. pl_e2e_test-0.2.1/src/prairielearn_e2e/ui.py +158 -0
  25. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/tests/stub_git.py +33 -7
  26. pl_e2e_test-0.2.1/tests/test_cli.py +294 -0
  27. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/tests/test_config.py +45 -1
  28. pl_e2e_test-0.2.1/tests/test_demo_cli.py +51 -0
  29. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/tests/test_fuzz.py +253 -26
  30. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/tests/test_git.py +25 -1
  31. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/tests/test_harness.py +34 -1
  32. pl_e2e_test-0.2.1/tests/test_instances.py +303 -0
  33. pl_e2e_test-0.2.1/tests/test_options.py +17 -0
  34. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/tests/test_parallel.py +221 -18
  35. pl_e2e_test-0.2.1/tests/test_publish_version.py +479 -0
  36. pl_e2e_test-0.2.1/tests/test_pytest_plugin.py +558 -0
  37. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/tests/test_regression.py +1 -2
  38. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/tests/test_reporting.py +60 -2
  39. pl_e2e_test-0.2.1/tests/test_repository.py +678 -0
  40. pl_e2e_test-0.2.1/tests/test_runtime.py +498 -0
  41. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/tests/test_sharding.py +1 -2
  42. pl_e2e_test-0.1.8/src/prairielearn_e2e/__init__.py +0 -23
  43. pl_e2e_test-0.1.8/src/prairielearn_e2e/cli.py +0 -58
  44. pl_e2e_test-0.1.8/src/prairielearn_e2e/config.py +0 -68
  45. pl_e2e_test-0.1.8/src/prairielearn_e2e/fuzz.py +0 -315
  46. pl_e2e_test-0.1.8/src/prairielearn_e2e/parallel.py +0 -250
  47. pl_e2e_test-0.1.8/src/prairielearn_e2e/repository.py +0 -298
  48. pl_e2e_test-0.1.8/src/prairielearn_e2e/runner.py +0 -28
  49. pl_e2e_test-0.1.8/tests/test_cli.py +0 -81
  50. pl_e2e_test-0.1.8/tests/test_publish_version.py +0 -169
  51. pl_e2e_test-0.1.8/tests/test_pytest_plugin.py +0 -202
  52. pl_e2e_test-0.1.8/tests/test_repository.py +0 -334
  53. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/LICENSE +0 -0
  54. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/setup.cfg +0 -0
  55. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/pl_e2e_test.egg-info/dependency_links.txt +0 -0
  56. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/pl_e2e_test.egg-info/entry_points.txt +0 -0
  57. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/pl_e2e_test.egg-info/top_level.txt +0 -0
  58. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/prairielearn_e2e/__main__.py +0 -0
  59. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/prairielearn_e2e/backend.py +0 -0
  60. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/prairielearn_e2e/contract.py +0 -0
  61. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/prairielearn_e2e/discovery.py +0 -0
  62. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/prairielearn_e2e/errors.py +0 -0
  63. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/prairielearn_e2e/expected_errors.py +0 -0
  64. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/prairielearn_e2e/harness.py +0 -0
  65. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/prairielearn_e2e/html_checks.py +0 -0
  66. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/prairielearn_e2e/metadata.py +0 -0
  67. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/prairielearn_e2e/question_tests.py +0 -0
  68. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/src/prairielearn_e2e/server.py +0 -0
  69. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/tests/__init__.py +0 -0
  70. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/tests/conftest.py +0 -0
  71. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/tests/test_contract.py +0 -0
  72. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/tests/test_discovery.py +0 -0
  73. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/tests/test_html_checks.py +0 -0
  74. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/tests/test_metadata.py +0 -0
  75. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/tests/test_question_tests.py +0 -0
  76. {pl_e2e_test-0.1.8 → pl_e2e_test-0.2.1}/tests/test_server.py +0 -0
@@ -1,2 +1,3 @@
1
1
  graft tests
2
+ include scripts/demo_cli.py
2
3
  global-exclude *.py[cod]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pl-e2e-test
3
- Version: 0.1.8
3
+ Version: 0.2.1
4
4
  Summary: Fuzz PrairieLearn question lifecycles against the real Python element runtime
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/PrairieLearn/pl-e2e-test
@@ -10,6 +10,7 @@ Requires-Python: >=3.13
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
12
  Requires-Dist: chevron==0.14.0
13
+ Requires-Dist: cyclopts<6,>=5.3
13
14
  Requires-Dist: filelock>=3.20
14
15
  Requires-Dist: jsonschema>=4.26
15
16
  Requires-Dist: lxml>=6.1
@@ -20,6 +21,7 @@ Requires-Dist: pyright>=1.1.414
20
21
  Requires-Dist: pytest>=9.1
21
22
  Requires-Dist: rich>=14
22
23
  Requires-Dist: ruff>=0.16
24
+ Requires-Dist: tomlkit>=0.15.2
23
25
  Dynamic: license-file
24
26
 
25
27
  # pl-e2e-test
@@ -32,19 +34,24 @@ instead of copying the harness and its support scripts.
32
34
  The package installs the reusable `prairielearn_e2e` Python API and a single
33
35
  `pl-e2e-test` command:
34
36
 
35
- - `pl-e2e-test` (no command, or any pytest/harness options) runs the installed
36
- lifecycle pytest suite.
37
+ - `pl-e2e-test run` runs the installed lifecycle pytest suite.
37
38
  - `pl-e2e-test diff` fuzzes questions selected from a Git diff.
38
- - `pl-e2e-test parallel` runs local shards and merges their results.
39
+ - `pl-e2e-test parallel run` and `parallel diff` run local shards and merge them.
39
40
  - `pl-e2e-test add-seed` records a failing concrete variant seed.
40
41
  - `pl-e2e-test reports` merges parallel shard results into terminal and JUnit reports.
41
- - `pl-e2e-test version` prints the installed version.
42
- - `pl-e2e-test help [COMMAND]` prints top-level help, or one command's help.
42
+ - `pl-e2e-test instances` shows configured, installed, cached, and local PrairieLearn
43
+ revisions.
44
+ - `pl-e2e-test version [REVISION]` reads or updates the configured PrairieLearn revision.
45
+ - `pl-e2e-test cache clean` clears the managed PrairieLearn instance cache.
46
+ - `pl-e2e-test --version` prints the installed CLI version.
47
+ - `pl-e2e-test help [COMMAND...]` prints top-level or nested command help. A bare
48
+ invocation also prints top-level help.
43
49
 
44
50
  ## Requirements and installation
45
51
 
46
52
  - Python 3.13 or newer
47
53
  - Git 2.30 or newer
54
+ - uv 0.9 or newer
48
55
 
49
56
  The harness depends on pytest, Pyright, Ruff, Chevron, JSON Schema, lxml, NumPy,
50
57
  and PrairieLearn. PrairieLearn's Python package is currently consumed from its Git
@@ -80,20 +87,37 @@ Pass a question, a directory of questions, or an `info.json` file. Repeat
80
87
  `--question-path` to select multiple roots:
81
88
 
82
89
  ```sh
83
- uv run pl-e2e-test \
90
+ uv run pl-e2e-test run \
84
91
  --question-path questions \
85
92
  --seed-count 3
86
93
  ```
87
94
 
88
95
  The installed suite exposes separate `generate`, `prepare`, `render`, and `grade`
89
- pytest markers. Any remaining arguments are passed to pytest:
96
+ pytest markers. Put pytest selection and execution arguments after `--`:
90
97
 
91
98
  ```sh
92
- uv run pl-e2e-test \
99
+ uv run pl-e2e-test -vv run \
93
100
  --question-path questions/chapter/example \
94
- --fuzz-seeds -m render -x -vv
101
+ --fuzz-seeds -- -m render -x
95
102
  ```
96
103
 
104
+ The native reporter owns progress, capture, tracebacks, summaries, and color. Use
105
+ repeatable global `-q` or `-v` flags and `--color auto|always|never`; pytest
106
+ presentation flags such as `-q`, `-v`, `--tb`, `-r`, `--color`, and `-s` are rejected
107
+ after `--`. `NO_COLOR`, `FORCE_COLOR`, and `CLICOLOR_FORCE` are also honored.
108
+
109
+ To preview the terminal UX without a PrairieLearn course or network access, run:
110
+
111
+ ```sh
112
+ uv run python scripts/demo_cli.py
113
+ uv run python scripts/demo_cli.py --color always mixed verbose cli-error
114
+ uv run python scripts/demo_cli.py --list
115
+ ```
116
+
117
+ The demo covers help and CLI errors plus passing, failing, skipped, xfailed/xpassed,
118
+ warning, collection-error, empty-selection, quiet, verbose, and per-shard progress
119
+ output.
120
+
97
121
  `--fuzz-seeds` creates and reports a random 64-bit master seed. Reproduce the same
98
122
  variant set with `--fuzz-seed N`. Without either option, deterministic seeds start at
99
123
  zero. Question metadata with `singleVariant: true` always uses seed zero.
@@ -103,22 +127,23 @@ zero. Question metadata with `singleVariant: true` always uses seed zero.
103
127
  Run multiple shards in one machine or CI job, with six concurrent processes by default:
104
128
 
105
129
  ```sh
106
- uv run pl-e2e-test parallel --shard-count 6 run -- \
130
+ uv run pl-e2e-test parallel run --shard-count 6 \
107
131
  --question-path questions --fuzz-seeds
108
132
 
109
- uv run pl-e2e-test parallel --shard-count 6 --junitxml results.xml diff -- \
133
+ uv run pl-e2e-test parallel diff --shard-count 6 --junitxml results.xml \
110
134
  --base main --fuzz-seed 42
111
135
  ```
112
136
 
113
- Launcher options go before the required `run` or `diff` selector; arguments after
114
- `--` are forwarded to that command. Normal runs remain deterministic. For
137
+ Native options go after `parallel run` or `parallel diff` and before an optional `--`;
138
+ only pytest arguments follow that boundary. Normal runs remain deterministic. For
115
139
  `--fuzz-seeds` and diff runs, the launcher generates one shared master seed unless
116
140
  `--fuzz-seed` is supplied, and prints a command for replaying the run without sharding.
117
141
 
118
142
  Each shard runs in a separate Python process using the current environment and
119
- working directory. Terminals show Rich progress; redirected output prints completion
120
- lines. All shards finish before reports are merged, even when a test fails. Captured
121
- failure output and successful stderr are shown before the combined summary.
143
+ working directory. Terminals show aggregate progress; redirected output uses stable
144
+ start and summary lines. All shards finish before reports are merged, even when a test
145
+ fails. Raw child output is shown only when a launch or internal failure cannot produce
146
+ a usable report.
122
147
  Interrupting the launcher stops and reaps its children.
123
148
 
124
149
  Intermediate reports are temporary. Pass the launcher's `--junitxml` option to retain
@@ -145,19 +170,19 @@ Relative JUnit paths are resolved against the course root. With `junitxml=None`,
145
170
  merged output is temporary. Invalid launcher settings raise `ValueError`.
146
171
 
147
172
  Courses using `scripts/run_e2e_shards.py` can replace its invocation with
148
- `pl-e2e-test parallel`, move `--fuzz-seed` into the forwarded arguments, and explicitly
173
+ `pl-e2e-test parallel run`, move `--fuzz-seed` into the native arguments, and explicitly
149
174
  pass `--fuzz-seeds` for full-suite runs to preserve the script's fuzzing behavior.
150
175
  For example:
151
176
 
152
177
  ```make
153
178
  test-e2e-sharded:
154
- uv run --active pl-e2e-test parallel --shard-count "$(E2E_SHARD_COUNT)" run -- \
155
- $(E2E_TEST_ARGS) $(E2E_PRAIRIELEARN_ARG) --fuzz-seeds $(E2E_FUZZ_SEED_ARG) $(PYTEST_ARGS)
179
+ uv run --active pl-e2e-test parallel run --shard-count "$(E2E_SHARD_COUNT)" \
180
+ $(E2E_TEST_ARGS) $(E2E_PRAIRIELEARN_ARG) --fuzz-seeds $(E2E_FUZZ_SEED_ARG) -- $(PYTEST_ARGS)
156
181
 
157
182
  test-e2e-fuzz-sharded:
158
- uv run --active pl-e2e-test parallel --shard-count "$(E2E_SHARD_COUNT)" diff -- \
183
+ uv run --active pl-e2e-test parallel diff --shard-count "$(E2E_SHARD_COUNT)" \
159
184
  --base "$(E2E_DIFF_BASE)" --head "$(E2E_DIFF_HEAD)" \
160
- --seed-count "$(E2E_FUZZ_SEED_COUNT)" $(E2E_FUZZ_SEED_ARG) $(E2E_PRAIRIELEARN_ARG) $(PYTEST_ARGS)
185
+ --seed-count "$(E2E_FUZZ_SEED_COUNT)" $(E2E_FUZZ_SEED_ARG) $(E2E_PRAIRIELEARN_ARG) -- $(PYTEST_ARGS)
161
186
  ```
162
187
 
163
188
  ### CI sharding
@@ -199,7 +224,7 @@ jobs:
199
224
  - run: uv python install && uv sync --locked
200
225
  - name: Run shard
201
226
  run: |
202
- uv run pl-e2e-test \
227
+ uv run pl-e2e-test run \
203
228
  --question-path questions \
204
229
  --seed-count 10 \
205
230
  --fuzz-seed "${{ github.run_id }}" \
@@ -241,19 +266,62 @@ jobs:
241
266
  ```
242
267
 
243
268
  The course must provide `.prairielearn/schemas/infoQuestion.json`. The harness checks
244
- question metadata against that schema and uses the exact PrairieLearn revision
245
- installed in the uv environment. It stores a bare, partial PrairieLearn repository in
246
- the platform-standard user cache directory. The first use of a new Git source or
247
- revision requires network access; later runs reuse that exact revision offline. The
248
- uv lockfile, rather than the cache, decides which revision is tested.
269
+ question metadata against that schema and resolves the configured PrairieLearn version
270
+ before each run. The default, `latest`, resolves the Git source's remote `HEAD`; a full
271
+ commit, branch, or unambiguous tag can be selected instead. Moving selectors require
272
+ network access and never silently fall back to stale cache state.
273
+
274
+ The selected revision's Python package and core elements are always used together. The
275
+ harness keeps a blob-filtered bare repository in the platform-standard user cache and
276
+ hydrates only `apps/prairielearn/python` and `apps/prairielearn/elements`. It launches
277
+ the selected Python package through a frozen uv overlay, without changing the course's
278
+ `pyproject.toml`, `uv.lock`, or virtual environment. A previously hydrated exact commit
279
+ can be reused offline when its uv dependencies are also cached.
249
280
 
250
281
  To use an existing PrairieLearn worktree or bare repository instead, pass
251
282
  `--prairielearn-path /path/to/PrairieLearn`. The repository must already contain the
252
- installed revision. An explicit path is authoritative: the harness does not fetch into
253
- it or fall back to the managed cache. In either mode, only the pinned revision's Python
254
- element controllers are archived into the test run's temporary directory. Standard
255
- cache environment settings such as `XDG_CACHE_HOME` can relocate the managed cache;
256
- CI systems can persist that directory between runs if desired.
283
+ selected revision. An explicit path is authoritative: the harness resolves selectors
284
+ against its local refs, does not fetch into it, and does not fall back to the managed
285
+ cache. For a local repository, `latest` means its current `HEAD`.
286
+
287
+ Inspect available revisions without contacting remotes:
288
+
289
+ ```sh
290
+ uv run pl-e2e-test instances
291
+ ```
292
+
293
+ Add `--refresh` to resolve the configured moving selector and verify remote access.
294
+ The table distinguishes fully hydrated runtime revisions from legacy element-only cache
295
+ entries. Standard cache environment settings such as `XDG_CACHE_HOME` can relocate the
296
+ managed cache; CI systems can persist that directory between runs if desired.
297
+ Cache use refreshes each repository or hydrated runtime's last-use timestamp. Whenever
298
+ the managed cache is used, unlocked items that have been idle for more than 30 days are
299
+ removed automatically across all configured PrairieLearn sources.
300
+
301
+ Read the effective configured selector without resolving it or contacting the network:
302
+
303
+ ```sh
304
+ uv run pl-e2e-test version
305
+ ```
306
+
307
+ Set `tool.pl-e2e-test.prairielearnVersion` in an existing `pyproject.toml` to a full
308
+ commit or verified PrairieLearn release tag with:
309
+
310
+ ```sh
311
+ uv run pl-e2e-test version 3.2.0
312
+ ```
313
+
314
+ Release tags are verified against the configured PrairieLearn Git source before the
315
+ file is changed. Use `--config PATH` to update another existing TOML file. Comments and
316
+ unrelated formatting are preserved, and a missing file is not created.
317
+
318
+ Remove all managed bare repositories and hydrated runtime revisions with:
319
+
320
+ ```sh
321
+ uv run pl-e2e-test cache clean
322
+ ```
323
+
324
+ The next test run fetches and hydrates its selected PrairieLearn revision again.
257
325
 
258
326
  ### Harness configuration
259
327
 
@@ -261,19 +329,25 @@ Selector settings are read automatically from the course's `pyproject.toml`:
261
329
 
262
330
  ```toml
263
331
  [tool.pl-e2e-test]
332
+ prairielearnVersion = "latest"
264
333
  elementSolutionSelectors = ["pl-sketch"]
265
334
  nonGradableSelectors = ['pl-sketch[read-only="true"]', 'pl-sketch[read-only="1"]']
266
335
  ```
267
336
 
268
- These are the defaults. `elementSolutionSelectors` identifies elements that provide
337
+ These are the defaults. Omitting `prairielearnVersion` is equivalent to `"latest"`.
338
+ Use a full commit for reproducible or offline runs, or a branch/tag when a named moving
339
+ target is intentional. `--prairielearn-version SELECTOR` overrides the TOML value for
340
+ one invocation, and replay commands always print the resolved full commit.
341
+
342
+ `elementSolutionSelectors` identifies elements that provide
269
343
  solutions without a `correct_answers` entry. It only relaxes that entry requirement;
270
344
  it does not generate a test submission. `nonGradableSelectors` excludes display-only
271
345
  answers from grading checks. Each supplied list replaces its default; omitted settings
272
346
  keep their defaults, and `[]` disables a list.
273
347
 
274
348
  Use `--config path/to/settings.toml` to select another TOML file with the same
275
- `[tool.pl-e2e-test]` table. This works with normal runs, `diff`, and arguments after
276
- `parallel run` or `parallel diff`. Explicit paths are relative to the working directory.
349
+ `[tool.pl-e2e-test]` table. This works with `run`, `diff`, `parallel run`, and
350
+ `parallel diff`. Explicit paths are relative to the working directory.
277
351
 
278
352
  ### Lifecycle JSON boundary
279
353
 
@@ -427,7 +501,7 @@ diffignore = [
427
501
 
428
502
  Rules use Git's native ignore syntax and are evaluated in order, so later rules may
429
503
  negate earlier ones with `!`. A missing `pyproject.toml`, tool table, or `diffignore`
430
- entry means that no paths are ignored. Additional arguments are forwarded to pytest.
504
+ entry means that no paths are ignored. Additional pytest arguments must follow `--`.
431
505
 
432
506
  To migrate from `.question-e2e.diffignore`, copy its non-comment rules into the
433
507
  `diffignore` array. The former file and `--e2ediffignore` option are no longer used.
@@ -455,12 +529,16 @@ course's TOML settings, or `config=load_config("settings.toml")` for an explicit
455
529
  `load_config` and `HarnessConfig` are exported from `prairielearn_e2e`. Omitting
456
530
  `config` uses the built-in defaults.
457
531
 
532
+ The command-line runner and high-level `run_parallel` API perform version resolution
533
+ and uv bootstrapping. Manually constructed `PrairieLearnBackend` and `QuestionHarness`
534
+ objects continue to use the PrairieLearn package already loaded by their interpreter.
535
+
458
536
  ## Development
459
537
 
460
538
  This project uses uv 0.9 or newer:
461
539
 
462
540
  ```sh
463
- uv sync
541
+ make install
464
542
  make test
465
543
  make format
466
544
  make smoke-dist
@@ -472,44 +550,36 @@ into an isolated target, and imports it using the locked development dependencie
472
550
 
473
551
  ## Publishing
474
552
 
475
- There are three supported publishing paths. Choose one for a release; do not run the
476
- local and GitHub paths for the same version because PyPI versions are immutable.
477
-
478
- ### GitHub-hosted trusted publishing
479
-
480
553
  Configure the PyPI project to trust the `publish.yml` workflow in the GitHub `pypi`
481
- environment. From a clean branch, run:
554
+ environment. Releases must start from a clean `main` that is synchronized with
555
+ `origin/main`, with `gh` authenticated and the publishing workflow available. To bump
556
+ the current version, run one of:
482
557
 
483
558
  ```sh
484
- make publish-version VERSION=0.2.0
559
+ make publish patch
560
+ make publish minor
561
+ make publish major
485
562
  ```
486
563
 
487
- This installs Python 3.13, performs the same locked dependency sync used in CI, runs
488
- the full checks, updates `pyproject.toml` and `uv.lock`, builds and smoke-tests the
489
- distributions, creates a release commit and annotated `v0.2.0` tag, and atomically
490
- pushes the branch and tag. The tag starts the workflow on `ubuntu-latest`; the workflow
491
- rechecks the tag/version match, tests, builds, generates attestations, and publishes
492
- with PyPI trusted-publishing OIDC credentials. Use `REMOTE=name` for a different Git
493
- remote.
494
-
495
- If an unpushed commit already contains the requested version, use the guarded bypass:
564
+ If the synchronized commit already contains the intended, not-yet-tagged version, use:
496
565
 
497
566
  ```sh
498
- make publish-version VERSION=0.2.0 ALLOW_PREBUMPED_VERSION=1
567
+ make publish current
499
568
  ```
500
569
 
501
- ### Direct local publishing
502
-
503
- To publish the version already present in `pyproject.toml` directly from this machine,
504
- configure a uv-supported PyPI credential such as `UV_PUBLISH_TOKEN`, then run:
570
+ The release command runs the shared `make release-check` gate, updates
571
+ `pyproject.toml` and `uv.lock` when needed, creates a release commit and annotated tag,
572
+ atomically pushes `main` and the tag, and watches the trusted-publishing workflow to
573
+ completion. Preview the full process without changing files, refs, remotes, or workflows
574
+ with:
505
575
 
506
576
  ```sh
507
- make publish-local
577
+ make publish patch DRY_RUN=1
508
578
  ```
509
579
 
510
- This runs all checks, builds and smoke-tests the current wheel and source archive, and
511
- passes only those two version-matched files to `uv publish`. It does not create a Git
512
- commit or tag.
580
+ If the release tag exists but trusted publishing needs to be dispatched again, run
581
+ `make publish-pypi`. This validates that the tag matches the current version and is
582
+ reachable from `origin/main`, then dispatches and watches a new workflow run.
513
583
 
514
584
  ### Local self-hosted GitHub runner
515
585
 
@@ -518,11 +588,11 @@ inside Docker. The container has no host mounts and does not receive the Docker
518
588
  Authenticate `gh` with repository administration permission, then run:
519
589
 
520
590
  ```sh
521
- make runner-start
522
- make publish-self-hosted RELEASE_TAG=v0.2.0
591
+ make publish-self-hosted
523
592
  ```
524
593
 
525
- The tag must already exist. This dispatches `publish.yml` from `main` with the
526
- `self-hosted` runner choice. `make runner-ensure` starts Docker Desktop on macOS when
527
- needed and repairs the runner before dispatch. Other lifecycle commands are
528
- `runner-status`, `runner-stop`, `runner-remove`, `runner-logs`, and `clean-runner`.
594
+ The current version's tag must already exist. This ensures the local runner is online,
595
+ dispatches `publish.yml` from `main` with the `self-hosted` runner choice, and watches
596
+ the run. `make runner-ensure` starts Docker Desktop on macOS when needed and repairs the
597
+ runner before dispatch. Other lifecycle commands are `runner-status`, `runner-stop`,
598
+ `runner-remove`, `runner-logs`, and `clean-runner`.