steppy-dev 0.3.0__py3-none-any.whl
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.
- steppy/CHANGELOG.md +862 -0
- steppy/__init__.py +9 -0
- steppy/__main__.py +13 -0
- steppy/_cli_common.py +183 -0
- steppy/_docker/Dockerfile.installed +53 -0
- steppy/_parallel/__init__.py +7 -0
- steppy/_parallel/compose.py +349 -0
- steppy/_parallel/event_bus.py +418 -0
- steppy/_parallel/failures.py +267 -0
- steppy/_parallel/fingerprint.py +141 -0
- steppy/_parallel/image.py +380 -0
- steppy/_parallel/lastfailed.py +125 -0
- steppy/_parallel/lifecycle.py +90 -0
- steppy/_parallel/preauth.py +109 -0
- steppy/_parallel/progress.py +101 -0
- steppy/_parallel/queue.py +385 -0
- steppy/_parallel/renderer.py +489 -0
- steppy/_parallel/retry.py +316 -0
- steppy/_player_bundle/THIRD_PARTY_LICENSES.txt +158 -0
- steppy/_player_bundle/__init__.py +100 -0
- steppy/_player_bundle/steppy-cursor-legacy.js +2 -0
- steppy/_player_bundle/steppy-cursor-legacy.js.map +1 -0
- steppy/_player_bundle/steppy-player.js +5419 -0
- steppy/_player_bundle/steppy-player.js.map +1 -0
- steppy/_player_bundle/steppy-video.js +472 -0
- steppy/_player_bundle/steppy-video.js.map +1 -0
- steppy/_templating.py +117 -0
- steppy/assertions.py +251 -0
- steppy/auth.py +493 -0
- steppy/build.py +2798 -0
- steppy/build_models.py +669 -0
- steppy/buildid.py +134 -0
- steppy/bundle/__init__.py +47 -0
- steppy/bundle/archive.py +239 -0
- steppy/bundle/classify.py +329 -0
- steppy/bundle/create.py +490 -0
- steppy/bundle/hashing.py +28 -0
- steppy/bundle/manifest.py +334 -0
- steppy/bundle/results.py +546 -0
- steppy/bundle_cli.py +455 -0
- steppy/captures.py +207 -0
- steppy/cli.py +2489 -0
- steppy/cloud/__init__.py +58 -0
- steppy/cloud/client.py +267 -0
- steppy/cloud/upload.py +497 -0
- steppy/compat.py +109 -0
- steppy/config.py +231 -0
- steppy/config_schema.py +87 -0
- steppy/contracts/__init__.py +54 -0
- steppy/contracts/bundle.py +215 -0
- steppy/contracts/report.py +346 -0
- steppy/contracts/tours.py +505 -0
- steppy/coordinator.py +1352 -0
- steppy/coverage/__init__.py +9 -0
- steppy/coverage/gaps.py +190 -0
- steppy/coverage/manifest.py +193 -0
- steppy/coverage/merge.py +363 -0
- steppy/coverage/normalize.py +174 -0
- steppy/coverage/report.py +178 -0
- steppy/coverage/sourcemap.py +219 -0
- steppy/coverage/store.py +473 -0
- steppy/coverage/triage.py +151 -0
- steppy/coverage_cli.py +265 -0
- steppy/durations.py +48 -0
- steppy/exclusive.py +178 -0
- steppy/flaky_cli.py +169 -0
- steppy/generator.py +633 -0
- steppy/guide.py +1467 -0
- steppy/history.py +352 -0
- steppy/init_cli.py +330 -0
- steppy/locales.py +282 -0
- steppy/migrate/__init__.py +35 -0
- steppy/migrate/apply.py +194 -0
- steppy/migrate/plan.py +199 -0
- steppy/migrate/rules.py +465 -0
- steppy/migrate/verify.py +163 -0
- steppy/migrate_cli.py +152 -0
- steppy/models.py +2539 -0
- steppy/naming.py +37 -0
- steppy/parser.py +475 -0
- steppy/pdf.py +193 -0
- steppy/presets.py +284 -0
- steppy/record_cli.py +796 -0
- steppy/report/__init__.py +37 -0
- steppy/report/assets.py +197 -0
- steppy/report/build.py +261 -0
- steppy/report/coverage_source.py +277 -0
- steppy/report/model.py +423 -0
- steppy/report/render.py +303 -0
- steppy/report/source.py +238 -0
- steppy/report_cli.py +173 -0
- steppy/runmeta.py +114 -0
- steppy/selection.py +183 -0
- steppy/sitemap.py +205 -0
- steppy/skills/steppy-app-tours/SKILL.md +448 -0
- steppy/skills/steppy-ci-report/SKILL.md +567 -0
- steppy/skills/steppy-scenario-author/SKILL.md +2176 -0
- steppy/skills/steppy-template/SKILL.md +676 -0
- steppy/templates/_steppy_events.py +291 -0
- steppy/templates/_steppy_tours.py +404 -0
- steppy/templates/angular/component-interactive.ts.j2 +109 -0
- steppy/templates/angular/component-video.ts.j2 +112 -0
- steppy/templates/angular/component.ts.j2 +229 -0
- steppy/templates/angular/content.ts.j2 +31 -0
- steppy/templates/angular/gitignore.j2 +3 -0
- steppy/templates/angular/group.component.ts.j2 +39 -0
- steppy/templates/angular/onboarding.component.ts.j2 +43 -0
- steppy/templates/angular/routes.ts.j2 +34 -0
- steppy/templates/ci/github-actions.yml +284 -0
- steppy/templates/ci/gitlab-ci.yml +178 -0
- steppy/templates/conftest.py.j2 +2405 -0
- steppy/templates/guide.book.html.j2 +145 -0
- steppy/templates/guide.fragment.html.j2 +257 -0
- steppy/templates/guide.html.j2 +301 -0
- steppy/templates/guide.md.j2 +18 -0
- steppy/templates/init/config.yaml +124 -0
- steppy/templates/init/credentials.yaml.example +30 -0
- steppy/templates/init/scenarios/example--first-flow.md +53 -0
- steppy/templates/report/_head.html.j2 +14 -0
- steppy/templates/report/_mark.html.j2 +3 -0
- steppy/templates/report/coverage.html.j2 +124 -0
- steppy/templates/report/index.html.j2 +203 -0
- steppy/templates/report/report.css +401 -0
- steppy/templates/report/scenario.html.j2 +137 -0
- steppy/templates/static/_headers.j2 +30 -0
- steppy/templates/static/_mode_links.html.j2 +22 -0
- steppy/templates/static/_seo_head.html.j2 +37 -0
- steppy/templates/static/cursor.js.j2 +10 -0
- steppy/templates/static/group.html.j2 +78 -0
- steppy/templates/static/guides.css +678 -0
- steppy/templates/static/index.html.j2 +79 -0
- steppy/templates/static/interactive.html.j2 +100 -0
- steppy/templates/static/onboarding.html.j2 +64 -0
- steppy/templates/static/page.html.j2 +145 -0
- steppy/templates/static/robots.txt.j2 +7 -0
- steppy/templates/static/video.html.j2 +135 -0
- steppy/templates/test_scenario.py.j2 +767 -0
- steppy/testids.py +166 -0
- steppy/totp.py +26 -0
- steppy/tour_spec.py +163 -0
- steppy/tours/__init__.py +22 -0
- steppy/tours/actions.py +402 -0
- steppy/tours/compiler.py +144 -0
- steppy/tours/diagnostics.py +137 -0
- steppy/tours/emit.py +239 -0
- steppy/tours/ids.py +40 -0
- steppy/tours/inference.py +399 -0
- steppy/tours/model.py +120 -0
- steppy/tours/paths.py +126 -0
- steppy/tours/pipeline.py +277 -0
- steppy/tours/publish.py +124 -0
- steppy/tours/replay.py +234 -0
- steppy/tours/routes.py +175 -0
- steppy/tours/settings.py +164 -0
- steppy/tours/text.py +237 -0
- steppy/tours_cli.py +219 -0
- steppy/tts/__init__.py +280 -0
- steppy/tts/cache.py +185 -0
- steppy/tts/presets.py +125 -0
- steppy/tts/providers/__init__.py +6 -0
- steppy/tts/providers/http.py +184 -0
- steppy/tts/providers/local.py +207 -0
- steppy/tts/providers/null.py +28 -0
- steppy/tts/registry.py +201 -0
- steppy/tts/track.py +158 -0
- steppy/tts/types.py +94 -0
- steppy/update_cli.py +460 -0
- steppy/video.py +730 -0
- steppy/video_cache.py +239 -0
- steppy/video_cli.py +271 -0
- steppy/worker.py +301 -0
- steppy_dev-0.3.0.dist-info/METADATA +1335 -0
- steppy_dev-0.3.0.dist-info/RECORD +177 -0
- steppy_dev-0.3.0.dist-info/WHEEL +4 -0
- steppy_dev-0.3.0.dist-info/entry_points.txt +2 -0
- steppy_dev-0.3.0.dist-info/licenses/LICENSE +202 -0
- steppy_dev-0.3.0.dist-info/licenses/NOTICE +13 -0
steppy/CHANGELOG.md
ADDED
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Steppy are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project aims to
|
|
5
|
+
follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.3.0] - 2026-10-22
|
|
10
|
+
|
|
11
|
+
The first public release, under the Apache-2.0 licence. This entry captures
|
|
12
|
+
everything since 0.1.2, the last tagged build: a pre-release hardening pass,
|
|
13
|
+
in-app tours and the other new commands, grouped by theme rather than by
|
|
14
|
+
individual commit.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **In-app tours (opt-in).** The scenarios that test and document a flow now also
|
|
18
|
+
compile into guided popovers over the live application — and CI replays every
|
|
19
|
+
tour. Enable `tours:` in `config.yaml` and add `tour: true` to a scenario:
|
|
20
|
+
`steppy generate` writes the tour files (`output/tours/<build-id>/` plus a flat
|
|
21
|
+
copy into your app's assets) and appends `test_<slug>__tour` to the scenario's
|
|
22
|
+
test, which follows the tour the way a user would and fails — naming the tour,
|
|
23
|
+
the step and the test-id — when the app no longer matches it. `steppy tours`
|
|
24
|
+
writes the files alone (`--check` gates CI on a committed app-assets copy whose
|
|
25
|
+
content is stale, compared for the build id that copy records, so a fresh
|
|
26
|
+
checkout under `git-sha` passes); `steppy build` (static) republishes them for
|
|
27
|
+
remote mode. A step points at one test-id value,
|
|
28
|
+
never a selector; text comes from `guide-text` or a new `tour-text`, with
|
|
29
|
+
`tour: skip|keep`, `tour-advance` and `tour-placement` to adjust inference. The
|
|
30
|
+
overlay is `<steppy-tour-host>`, a new `@steppy/player/tour` entry with
|
|
31
|
+
type declarations: data-only tour files validated whole, test-id lookup in the
|
|
32
|
+
light DOM, top-layer stacking, keyboard and screen-reader support, resume
|
|
33
|
+
across page loads, remote loading pinned by SRI, and no inline script or style
|
|
34
|
+
(strict CSP and Trusted Types work). Full reference: `docs/tours.md`; design:
|
|
35
|
+
`docs/design/in-app-tours.md`; a working demo: `fixtures/tour-demo/`. Also part
|
|
36
|
+
of the feature:
|
|
37
|
+
- **Localization.** One file per tour and locale with text in it; text falls
|
|
38
|
+
back exact tag → base language, never to another language (a step with no
|
|
39
|
+
text shows a generic hint in its own language). The overlay picks the file
|
|
40
|
+
from the host's `locale` or `<html lang>`, follows a language switch
|
|
41
|
+
mid-tour (including one made while the tour loads), and sets `lang`/`dir`
|
|
42
|
+
on the popover (right-to-left languages mirror). Buttons and hints ship in
|
|
43
|
+
English, French, Dutch and German; other tour languages get a
|
|
44
|
+
`chrome-strings-missing` warning and take the host's `strings` property.
|
|
45
|
+
New warnings `locale-not-emitted` and "did you mean" for locale keys that
|
|
46
|
+
differ only in case or `-`/`_`.
|
|
47
|
+
- **Phones, drawers and dialogs.** On a small screen the popover is a sheet at
|
|
48
|
+
the top or bottom, away from what the user taps; it follows a drawer that
|
|
49
|
+
slides in and a target scrolled into view. A step with nothing to point at
|
|
50
|
+
(a `confirm` kept with `tour: keep`) is centred, except over an open app
|
|
51
|
+
dialog: there it becomes a sheet, so it never hides the question it is
|
|
52
|
+
about.
|
|
53
|
+
- **Remote files are pinned by the bundle.** When a bundled manifest of the same
|
|
54
|
+
build exists, any other manifest (a guide site or a same-origin proxy) only
|
|
55
|
+
locates files: tour, locale and SRI hash come from the bundle.
|
|
56
|
+
- **Writes into your app are guarded.** An `app-assets` folder is never a
|
|
57
|
+
project root or a folder Steppy owns, a foreign `tours.manifest.json` stops
|
|
58
|
+
the run, and only Steppy tour files are ever overwritten or deleted.
|
|
59
|
+
- New config key `output.tours_dir` (default `./output/tours`); the generated
|
|
60
|
+
`tests/e2e/_steppy_tours.py` replay helper; `steppy guide --all` and
|
|
61
|
+
`steppy validate` compile tours too.
|
|
62
|
+
- With tours enabled, generated tests set `__STEPPY_TOURS_DISABLED__` in
|
|
63
|
+
every browser context (an app that auto-starts a tour cannot cover what a test
|
|
64
|
+
clicks); `steppy run --guide` deselects the replays.
|
|
65
|
+
- The static build gains a `tours` key in the site `manifest.json`, a CORS block
|
|
66
|
+
for `/tours/*` in `_headers`, and reserves the guide slug `tours`.
|
|
67
|
+
- `steppy tours --check` says when committed tour files differ only in line
|
|
68
|
+
endings (git converted them on checkout, which regenerating cannot fix);
|
|
69
|
+
`guide-origin` accepts a URL and keeps its origin; `STEPPY_TOUR_EXPECT_BUILD_ID`
|
|
70
|
+
reaches the replays under `--workers N`.
|
|
71
|
+
- **Angular adapter, `@steppy/angular`** (`packages/angular`, Angular 20–22,
|
|
72
|
+
zone.js or zoneless): `provideSteppyTours()`, a `<steppy-tours>`
|
|
73
|
+
component placed once in the shell, a `SteppyTours` service (`start`,
|
|
74
|
+
`stop`, `next`; `active`, `current` and `lastError` signals; an `events`
|
|
75
|
+
stream) and a `steppyTourStart` directive. It reaches entry routes through
|
|
76
|
+
the Angular router inside the zone — a refused or redirected navigation fails
|
|
77
|
+
the tour at once — keeps the overlay's work outside the zone, follows the
|
|
78
|
+
app's i18n through a locale signal, and loads nothing on the server.
|
|
79
|
+
`fixtures/tour-demo-angular/` is the demo again in Angular, PrimeNG and
|
|
80
|
+
ngx-translate, driven unchanged by the plain demo's scenarios and replays.
|
|
81
|
+
- `@steppy/player` and `@steppy/angular` are published to npm. The player
|
|
82
|
+
gains type declarations for the guide entry too, a `typesVersions` fallback
|
|
83
|
+
for the tour entry, LICENSE, NOTICE and the third-party licenses in the
|
|
84
|
+
package, and a `check:pack` script that fails when `package.json` points at
|
|
85
|
+
a file `npm pack` would not ship. The `./manifest` subpath, which never
|
|
86
|
+
resolved, is gone. The release workflow publishes both once an `NPM_TOKEN`
|
|
87
|
+
secret exists.
|
|
88
|
+
- **Steppy is on PyPI**, as `steppy-dev`: `pip install steppy-dev`.
|
|
89
|
+
- **Steppy is open source under Apache-2.0.** `LICENSE` + `NOTICE` at the root,
|
|
90
|
+
declared as a PEP 639 `license = "Apache-2.0"` expression (hence
|
|
91
|
+
`hatchling>=1.27`) and shipped in the wheel's `dist-info/licenses/`. The npm
|
|
92
|
+
packages move to the same license (the player was `UNLICENSED`, the wrappers
|
|
93
|
+
MIT). The player bundles embed Lit (BSD-3-Clause) and Zod (MIT), whose full
|
|
94
|
+
texts now ship beside them as `_player_bundle/THIRD_PARTY_LICENSES.txt`.
|
|
95
|
+
`SECURITY.md` gains a private-reporting policy and `CONTRIBUTING.md` states the
|
|
96
|
+
inbound license. `pyproject.toml` now carries a description, readme, keywords
|
|
97
|
+
and classifiers.
|
|
98
|
+
- **`tests/test_no_private_names.py`** — scans every file git would publish for
|
|
99
|
+
client project names, private machine paths and the former example domain.
|
|
100
|
+
The deny-list lives in a gitignored local file rather than in the repository,
|
|
101
|
+
because a list there, even hashed, would publish what it guards; without the
|
|
102
|
+
file the check skips.
|
|
103
|
+
- **Neutral example domain.** Every example, fixture, test and doc that used the
|
|
104
|
+
former example domain now uses a generic one: roles `editor` and
|
|
105
|
+
`reviewer`, records `customer`, `invoice` and `product`. French and Dutch
|
|
106
|
+
strings were translated rather than swapped word for word. The demo fixture's
|
|
107
|
+
role and guide are now `fixtures/demo-guide/**/editor/invoices`.
|
|
108
|
+
- **CI:** a ruff lint + format job, a Python 3.12/3.13 matrix, and a check that the
|
|
109
|
+
committed player bundles match a fresh build of `packages/player/src/` (the
|
|
110
|
+
build is reproducible, so a mismatch means someone forgot to re-vendor).
|
|
111
|
+
- **`.gitattributes`** forcing LF on every OS, so the byte-mirror guard tests
|
|
112
|
+
cannot fail on a Windows checkout.
|
|
113
|
+
|
|
114
|
+
- **Six SEO follow-ups**, each of which the sitemap change surfaced and
|
|
115
|
+
deliberately left alone.
|
|
116
|
+
- **`manifest.json` and `<role>.nav.json` no longer advertise a mode with no
|
|
117
|
+
page.** Both emitted a guide's declared `modes` verbatim, so a guide
|
|
118
|
+
declaring `video` without `steppy build --video` told any consumer there was
|
|
119
|
+
a video page to link. The Angular toggle and the guide page's mode row
|
|
120
|
+
already filtered it; all four now go through one `_emitted_modes` helper.
|
|
121
|
+
*Behaviour change for anyone reading either file.*
|
|
122
|
+
- **Published PDFs are no longer the only indexable form of a `noindex`
|
|
123
|
+
page.** `handbook/<locale>.pdf` is Chromium printing a page that is
|
|
124
|
+
deliberately out of the index, and `<slug>/<locale>.pdf` competes with its
|
|
125
|
+
own HTML — but a PDF has no `<head>` to carry a robots meta. Every static
|
|
126
|
+
build now emits a `_headers` sidecar (`X-Robots-Tag: noindex` on `/*.pdf`
|
|
127
|
+
public, `/*` private). **It does nothing on GitHub or GitLab Pages**, which
|
|
128
|
+
cannot serve custom headers and are what the shipped CI scaffolds target;
|
|
129
|
+
Netlify and Cloudflare Pages honour it. Same bargain as `.nojekyll`.
|
|
130
|
+
- **A build warning when `seo.baseUrl` carries a path.** `robots.txt` and
|
|
131
|
+
`_headers` are honoured only at the origin root, and a GitHub Pages *project*
|
|
132
|
+
site lives under `/<repo>/` — so the correct `baseUrl` there silently
|
|
133
|
+
disarmed both. Canonical links, `hreflang` and `sitemap.xml` were and are
|
|
134
|
+
unaffected.
|
|
135
|
+
- **The capture side validates locales too.** `config.yaml`'s `locales:`,
|
|
136
|
+
`steppy run --locales` and `steppy guide --locale` now reject a tag that is
|
|
137
|
+
not usable BCP-47, matching what `steppy build` has done since the hreflang
|
|
138
|
+
work. Each of them becomes a screenshot directory segment the other side
|
|
139
|
+
reads back, so accepting one only moved the failure somewhere it could not
|
|
140
|
+
be explained. `--layouts` is deliberately still unvalidated.
|
|
141
|
+
- **Regional locales resolve content through their bare language.**
|
|
142
|
+
`guide-text: { fr: … }` now serves `fr-BE` instead of failing the build with
|
|
143
|
+
`Missing guide-text for locale 'fr-BE'`, and the same rule reaches the
|
|
144
|
+
interactive manifest (which silently emitted nulls for the identical miss)
|
|
145
|
+
and the capture-time i18n switch. An exact `fr-BE:` key still wins.
|
|
146
|
+
*Behaviour change: a build that used to stop now succeeds.*
|
|
147
|
+
- **A locale the app cannot be switched into is warned about, not skipped.**
|
|
148
|
+
When `i18n.localstorage.values` has no entry for the active locale — not even
|
|
149
|
+
by language — the run previously returned quietly and captured a full set of
|
|
150
|
+
screenshots **in the app's default language**, filed under the locale's own
|
|
151
|
+
directory. Nothing at any stage reported it. The branch also had no test.
|
|
152
|
+
- **`interactive.html` links the page it canonicals to.** All three renderings
|
|
153
|
+
of a guide now carry the same format row from one shared partial, replacing
|
|
154
|
+
the interactive page's missing link and the video page's hand-rolled one
|
|
155
|
+
(whose label did not match `MODE_LABELS`). `.steppy-mode-links` also gained
|
|
156
|
+
the CSS it never had — the row had been rendering as a bulleted list.
|
|
157
|
+
- Internal: the three duplicated "link to the site index" ternaries in the
|
|
158
|
+
templates collapse to one `_index_href`, including the dead one that compared
|
|
159
|
+
`default_locale` with itself.
|
|
160
|
+
- **Standards-correct `sitemap.xml`, and one canonical URL per guide** (static
|
|
161
|
+
target, `seo.mode: 'public'`). Two problems, one change.
|
|
162
|
+
- **The sitemap is generated rather than templated.** `src/steppy/sitemap.py`
|
|
163
|
+
is a new stdlib-only leaf that builds the document with
|
|
164
|
+
`xml.etree.ElementTree`; `templates/static/sitemap.xml.j2` is no longer
|
|
165
|
+
bundled. Escaping is the weaker half of the argument — Jinja's `| e` and
|
|
166
|
+
ElementTree behave identically on element text — but the template
|
|
167
|
+
interpolated `hreflang="{{ alt.hreflang }}"` with **no filter at all** under
|
|
168
|
+
an `autoescape=False` environment, from a `locales` list nothing validated.
|
|
169
|
+
The stronger half: a schema-bearing machine format should have a generator,
|
|
170
|
+
`<lastmod>` becomes structural rather than textual, and the whole thing is
|
|
171
|
+
now a pure function with its own unit tests. Characters no XML document can
|
|
172
|
+
carry are rejected outright instead of silently emitted.
|
|
173
|
+
- **A project `templates/static/sitemap.xml.j2` still wins.** Nothing is
|
|
174
|
+
bundled, but an override is still the way to reach the parts of the protocol
|
|
175
|
+
Steppy does not emit (`<image:image>` over a guide's screenshots,
|
|
176
|
+
`<video:video>`, a `<sitemapindex>` split), and the template context is
|
|
177
|
+
unchanged, so any existing override keeps working untouched.
|
|
178
|
+
- **`interactive.html` and `video.html` now canonical to the guide's
|
|
179
|
+
default-locale step-by-step page** and are excluded from the sitemap. They
|
|
180
|
+
render the same steps in a different shell, so a guide was occupying three
|
|
181
|
+
or four near-identical indexable URLs.
|
|
182
|
+
- **The site root is canonical as `https://…/`, not `https://…/index.html`.**
|
|
183
|
+
`index.html` is still written and still serves those bytes; only the URL
|
|
184
|
+
Steppy declares changed, so the home page stops being indexed twice.
|
|
185
|
+
Internal links deliberately still point at `index.html` — the canonical does
|
|
186
|
+
the consolidating, and rewriting them would break opening a build over
|
|
187
|
+
`file://`.
|
|
188
|
+
- **Guides get a `<slug>/index.html` redirect stub**, which groups, onboarding
|
|
189
|
+
and the handbook already had. A bare `/<slug>/` used to 404. In public mode
|
|
190
|
+
every stub now carries an absolute canonical and, deliberately, no
|
|
191
|
+
`noindex`: a noindex page canonicalling into an indexable cluster is a known
|
|
192
|
+
footgun, and a zero-delay meta refresh already reads as a redirect.
|
|
193
|
+
- **New `seo.lastmod`** — a W3C datetime stamped on every sitemap entry.
|
|
194
|
+
Unset by default, which keeps a build byte-reproducible. There is
|
|
195
|
+
deliberately no `'auto'` mode: `actions/checkout` stamps every file at clone
|
|
196
|
+
time, so an mtime-derived value reads as "now" on every build. Feed it
|
|
197
|
+
`git log -1 --format=%cI`. `<changefreq>` and `<priority>` are still not
|
|
198
|
+
emitted — Google and Bing ignore them.
|
|
199
|
+
- **`locales` entries are validated as BCP-47** at config load, because one
|
|
200
|
+
malformed `hreflang` makes a search engine discard the *whole* alternates
|
|
201
|
+
cluster. The tag you write is kept **verbatim** in filenames and screenshot
|
|
202
|
+
directories — it is also your app's i18n key and your `guide-text:` map key
|
|
203
|
+
— while `hreflang`, `<html lang>` and `og:locale` get the canonical
|
|
204
|
+
spelling. So `fr_BE` is accepted and produces `<html lang="fr-BE">` at
|
|
205
|
+
`…/fr_BE.html`. Two locales that normalise to the same tag are rejected, as
|
|
206
|
+
is a locale that also names a layout.
|
|
207
|
+
- **`pick_locale` falls back to the bare language subtag.** Without it a site
|
|
208
|
+
on `['fr-BE', 'nl-BE']` whose `LocalizedText` maps are written `{fr:…, nl:…}`
|
|
209
|
+
served the *French* meta description on every `nl-BE` page while declaring
|
|
210
|
+
`lang="nl-BE"` — two URLs, identical text, different declared languages,
|
|
211
|
+
which is exactly what makes a crawler drop the cluster.
|
|
212
|
+
- **Behaviour change:** `og:locale` is now emitted only for a
|
|
213
|
+
`language_TERRITORY` locale. A public `openGraph: true` site on `locales:
|
|
214
|
+
['fr']` loses `<meta property="og:locale" content="fr">` — that value was
|
|
215
|
+
never a valid Open Graph locale, and inventing `fr_FR` would be a guess.
|
|
216
|
+
- **Exclusion groups (`exclusive:` in scenario frontmatter)** — mark scenarios that
|
|
217
|
+
must never run at the same time. Two scenarios naming the same group are
|
|
218
|
+
serialised against each other under `steppy run --workers N`; different groups
|
|
219
|
+
still run concurrently, and ungrouped scenarios are untouched. Work stealing
|
|
220
|
+
assumed every scenario was independent of every other; the only remedy for two
|
|
221
|
+
that are not was `--workers 1`, which serialises the whole suite.
|
|
222
|
+
- **A partition, not a protocol.** The coordinator collapses each group into a
|
|
223
|
+
single queue task before any container exists, and one worker runs its members
|
|
224
|
+
sequentially — one pytest process each, exactly as for an ungrouped scenario.
|
|
225
|
+
So there is no lock to leak when a worker is killed, no lease, no timeout to
|
|
226
|
+
tune, and no worker that ever waits on another. The only run-time primitive is
|
|
227
|
+
the O_EXCL claim that was already there.
|
|
228
|
+
- **Results, logs, JUnit and events stay keyed per scenario** even inside a
|
|
229
|
+
group, which is why failure artifacts, the last-failed cache, run history, the
|
|
230
|
+
flaky verdict and the renderer needed no changes at all — and why a worker
|
|
231
|
+
dying mid-group reduces to "which member has no result file".
|
|
232
|
+
- It costs wall clock: a run can never finish faster than its heaviest group's
|
|
233
|
+
total duration, so the seed order now weights a group by the **sum** of its
|
|
234
|
+
members' recorded medians. `steppy validate` warns about a group with a single
|
|
235
|
+
member, which is what a typo looks like.
|
|
236
|
+
- Two hazards closed on the way: a selection that collapses to one task used to
|
|
237
|
+
hit a branch that returned **0 without running anything** (the most obvious
|
|
238
|
+
first use of the feature — "every scenario hits one staging database" — would
|
|
239
|
+
have gone green having executed nothing), and a task that no worker ever
|
|
240
|
+
claimed was invisible to both orphan detection and result collection. Both now
|
|
241
|
+
fail the build, on both exit-code branches. A scenario that never started and
|
|
242
|
+
then passed on retry is reported as passed, not FLAKY, so the history
|
|
243
|
+
`steppy flaky` ranks from stays honest.
|
|
244
|
+
- **`steppy report`** — render one run to a self-contained static site: outcomes,
|
|
245
|
+
failure causes grouped so one broken selector across forty scenarios reads as
|
|
246
|
+
one row, **the failing step named** with its timeline and captures, coverage
|
|
247
|
+
buckets, a versioned `report.json` and a shields.io `badge.json`. Publishing it
|
|
248
|
+
to GitHub or GitLab Pages is a copy; both shipped CI templates carry the job,
|
|
249
|
+
commented out, with the warnings that decision deserves.
|
|
250
|
+
- **Three inputs, one report.** A live `output/` tree, a `.tar.zst`/`.tar.gz`
|
|
251
|
+
bundle, or a directory somebody unpacked one into. Archives are read in two
|
|
252
|
+
passes — `inspect_bundle` for the manifest, then one sequential streaming pass
|
|
253
|
+
for exactly the members requested — because the zstd stream is non-seekable
|
|
254
|
+
and there is no rewinding.
|
|
255
|
+
- **It never parses a path.** `bundle create` already stamped
|
|
256
|
+
scenario/locale/layout/step/frame onto every manifest row, so the whole report
|
|
257
|
+
is a manifest transform. Two read-side rules follow from a bundle possibly
|
|
258
|
+
coming from elsewhere: destinations are computed from validated components
|
|
259
|
+
rather than taken from a manifest path (streaming tar members bypasses
|
|
260
|
+
Python's extraction filters, and an arcname is attacker-controlled), and no
|
|
261
|
+
absolute filesystem path reaches `report.json` or the page footer, both of
|
|
262
|
+
which get published.
|
|
263
|
+
- **Honest about what it cannot know.** A `capabilities` block distinguishes "no
|
|
264
|
+
per-step timeline was recorded" from "the timeline was empty", a run with no
|
|
265
|
+
outcomes reads `unknown` rather than `passed`, and coverage rebuilt from a
|
|
266
|
+
detached bundle's raw records refuses to report assert *strength* — the
|
|
267
|
+
scenario files needed to count assert steps never travel, so every endpoint
|
|
268
|
+
would otherwise read as weakly covered.
|
|
269
|
+
- **The exit code describes rendering, not the run.** A report of a failing run
|
|
270
|
+
exits 0. A report step that goes red for a red run is a step people delete.
|
|
271
|
+
- `--screenshots failed` by default: the guide site is where every capture
|
|
272
|
+
lives, and `all` on a 4-locale × 2-layout corpus is hundreds of megabytes
|
|
273
|
+
pushed to Pages on every run. Captures identical across layouts copy once.
|
|
274
|
+
- **`coverage.non_ui`** — declare in committed config which endpoints and routes
|
|
275
|
+
can never have a UI test (`[<METHOD> ]<glob>`; health checks, webhooks, OAuth
|
|
276
|
+
callbacks, internal cron). The equivalent triage flags live in the gitignored
|
|
277
|
+
`.steppy/coverage.db`, so a fresh CI checkout had none and every such endpoint
|
|
278
|
+
counted against the denominator — a percentage wrong for reasons nobody could
|
|
279
|
+
fix from CI. Applied as a read-time overlay that never writes to the store, so
|
|
280
|
+
merge and refresh still never touch `triage`; an explicit `ui_testable` flag
|
|
281
|
+
still outranks a pattern, and a pattern matching nothing is reported rather than
|
|
282
|
+
silently inflating the number. Settles the badge as
|
|
283
|
+
**`tested / (declared − non_ui)`**, subtracted from *both* sides so it cannot
|
|
284
|
+
exceed 100%. `steppy coverage report` gained `--badge` / `--badge-surface`.
|
|
285
|
+
- **A third packaged Claude skill, `steppy-ci-report`**, installed by `steppy init`
|
|
286
|
+
beside `steppy-scenario-author` and `steppy-template`. The skills are split by
|
|
287
|
+
**job**, and shipping results is a third one: neither existing skill had a CI
|
|
288
|
+
section at all, and the coverage material in the author skill was four lines
|
|
289
|
+
that predated `non_ui`, the badge and the report. It covers the CI loop, the
|
|
290
|
+
report and its three inputs, publishing to Pages with the warnings that decision
|
|
291
|
+
deserves, the coverage map end to end (record → merge → refresh → gaps), the
|
|
292
|
+
badge formula, the four coverage tiers, and a troubleshooting table for the
|
|
293
|
+
symptoms that are otherwise silent — "badge says n/a", "coverage reads lower in
|
|
294
|
+
CI than locally", "report has no step detail". `steppy-template` gained the run
|
|
295
|
+
report's stylesheet (including the two ways `report.css` bites: it is parsed as a
|
|
296
|
+
template, and its environment autoescapes), and the author skill now routes
|
|
297
|
+
shipping questions rather than half-answering them.
|
|
298
|
+
- **A fourth packaged Claude skill, `steppy-app-tours`**, for the half of in-app
|
|
299
|
+
tours that happens in the app's own code: serving the tour files, installing the
|
|
300
|
+
packages, `@steppy/angular` (and Angular without it), plain JavaScript, React and
|
|
301
|
+
Vue, starting tours with entry params, the language and chrome strings, where
|
|
302
|
+
test-ids go in components, CSP and Trusted Types, events, the kill switch, and a
|
|
303
|
+
troubleshooting table for the running app. The author skill had eight lines on it,
|
|
304
|
+
and they were installed in the Steppy project, which is often not where the app's
|
|
305
|
+
code is. `steppy init --app-dir DIR` installs the skill into the app's repository
|
|
306
|
+
and records it in the project's manifest (as `../<app>/…`), so `steppy update`
|
|
307
|
+
refreshes it with the rest. The author skill's tour table gained the four
|
|
308
|
+
diagnostics it was missing (`no-tours`, `app-url-unresolved`,
|
|
309
|
+
`layout-media-overlap`, `app-assets-skipped`). A new guard,
|
|
310
|
+
`tests/test_skills_sync.py`, fails when a visible command, a tour diagnostic, or
|
|
311
|
+
the overlay's or the adapter's public API grows without the skills.
|
|
312
|
+
- **`steppy init --ci-gitlab`** and a shipped `templates/ci/gitlab-ci.yml`. A
|
|
313
|
+
second boolean rather than `--ci <vendor>`, because `--ci` is one people already
|
|
314
|
+
type. Worth knowing: GitLab Pages can be restricted to project members, which
|
|
315
|
+
is the one place it beats GitHub's for publishing a run report.
|
|
316
|
+
- **`steppy video cache`** — report, prune or clear the two render caches.
|
|
317
|
+
`.steppy/tts-cache/` and `.steppy/video-cache/` are content-addressed, which
|
|
318
|
+
is what makes them correct and what makes them grow forever: every edited
|
|
319
|
+
sentence leaves its old clip behind, a Piper upgrade orphans all of them at
|
|
320
|
+
once, and nothing ever collected them. `--prune-older-than <days>` and
|
|
321
|
+
`--max-size <MB>` evict least-recently-**used** entries first (the lookup
|
|
322
|
+
paths now touch what they read, so a clip rebuilt into every release stays
|
|
323
|
+
young), `--clear` empties, `--tts`/`--video` scope any of those to one cache,
|
|
324
|
+
and stale staging files are always swept. An entry is its primary file *plus*
|
|
325
|
+
its sidecars — dropping a `.mp4` and orphaning its `.vtt` would leave the
|
|
326
|
+
directory looking populated while every lookup missed. Mechanics live in the
|
|
327
|
+
stdlib-only leaf `steppy/video_cache.py`.
|
|
328
|
+
- **`steppy bundle upload`** — push one run's results to a Steppy control plane.
|
|
329
|
+
The client half of the content-addressed protocol the bundle contract
|
|
330
|
+
describes: the manifest is offered, the server returns presigned PUT URLs
|
|
331
|
+
**only for digests it does not already hold**, and finalize verifies every one
|
|
332
|
+
of them before ingestion is queued. On a real 240 MiB capture (3204 files
|
|
333
|
+
across 4 locales and 2 layouts) that is 130 MiB sent the first time and
|
|
334
|
+
**nothing at all** the second.
|
|
335
|
+
- **No archive is written.** The protocol is per-file, so a `.tar.zst` would
|
|
336
|
+
be created only to be read back and discarded. With no argument the command
|
|
337
|
+
uploads straight from `output/`; pass it a `.tar.zst`/`.tar.gz` to upload
|
|
338
|
+
one an earlier CI step built, and the two produce an identical digest set.
|
|
339
|
+
- **The endpoint comes from `--url` → `STEPPY_CLOUD_URL` → a new optional
|
|
340
|
+
`cloud.url` in `config.yaml`. The token comes from `--token` →
|
|
341
|
+
`STEPPY_CLOUD_TOKEN` and nowhere else** — there is deliberately no config
|
|
342
|
+
key for it, because that file is committed. Same line
|
|
343
|
+
`_check_credential_hygiene` already draws for app credentials.
|
|
344
|
+
- **Interrupted uploads resume for free.** The offer is keyed on the run and
|
|
345
|
+
reports what is already held, so running the command again sends only what
|
|
346
|
+
is missing; a presigned URL that expires mid-upload triggers one re-offer
|
|
347
|
+
and carries on. The bundle's own run id is the idempotency key, so a retry
|
|
348
|
+
attaches to the run the first attempt created rather than burning a second
|
|
349
|
+
run number.
|
|
350
|
+
- Steppy still works with **no control plane at all**, and nothing outside
|
|
351
|
+
`steppy.cloud` may assume one exists. The new package is lazy-imported and
|
|
352
|
+
guarded (`tests/test_no_cloud_import_on_default_path.py`), which keeps both
|
|
353
|
+
the `httpx` import cost and that boundary off the default path.
|
|
354
|
+
- **Results bundles** (`steppy run --bundle` + `steppy bundle create|inspect|verify`).
|
|
355
|
+
One run's artifacts as a single portable file: a `.tar.zst` holding the `output/`
|
|
356
|
+
tree **exactly as Steppy wrote it** — no path rewriting, so `tar -xf` followed by
|
|
357
|
+
`steppy guide` or `steppy build` works unchanged on a downloaded CI artifact —
|
|
358
|
+
plus a `manifest.json` describing and hashing every file, a merged `junit.xml`,
|
|
359
|
+
and per-test `events/*.jsonl`. Useful with nothing else installed: a
|
|
360
|
+
self-describing run archive to attach to a build. The manifest is a **versioned
|
|
361
|
+
wire contract** (`src/steppy/contracts/`, published as
|
|
362
|
+
`contracts/bundle-manifest.schema.json`), deliberately `extra="allow"` where the
|
|
363
|
+
config models are `extra="forbid"` — a config typo must fail loudly, a newer
|
|
364
|
+
producer's extra field must not.
|
|
365
|
+
- **Two opt-in artifact sinks, because a plain run produces neither.** Per-test
|
|
366
|
+
event records were previously `STEPPY-EVT::` lines scraped from container
|
|
367
|
+
stdout in parallel mode only, and JUnit XML was single-worker-only at a
|
|
368
|
+
user-configured path. `--bundle` turns on both, keyed by run id
|
|
369
|
+
(`output/events/<run-id>/`, `output/junit/<run-id>/`) so a previous run's
|
|
370
|
+
records can never end up in this run's bundle, and per *scenario* rather than
|
|
371
|
+
per worker so N workers sharing one `output/` mount do not overwrite each
|
|
372
|
+
other. Off by default: it changes the pytest invocation, and `steppy run` with
|
|
373
|
+
no flags stays byte-identical.
|
|
374
|
+
- **Attribution is read from pytest, never parsed out of a node id.** The events
|
|
375
|
+
plugin takes `(scenario, locale, layout)` from `item.callspec.params` and the
|
|
376
|
+
module's baked `SCENARIO`. Parsing would be wrong rather than merely awkward:
|
|
377
|
+
a cross-role scenario parametrizes two page fixtures and reads
|
|
378
|
+
`[fr-mobile-fr-mobile]`, and a locale like `fr-BE` contains the id's own
|
|
379
|
+
separator. Artifacts named after a *sanitised* node id (failure screenshots,
|
|
380
|
+
traces, videos) are attributed through a forward map instead, since that
|
|
381
|
+
sanitiser is lossy and cannot be inverted.
|
|
382
|
+
- **Archives are byte-reproducible**, which is what makes the content-addressed
|
|
383
|
+
upload protocol worth having — an unchanged run must not re-send everything.
|
|
384
|
+
Members sorted, timestamps and ownership normalised, PAX format, no directory
|
|
385
|
+
entries, the run's creation time taken from the run rather than the clock, and
|
|
386
|
+
the gzip header stripped of both its timestamp and the embedded output
|
|
387
|
+
filename.
|
|
388
|
+
- `truncated` in the manifest means the size budget dropped something that
|
|
389
|
+
belonged in the bundle — never an artifact the user excluded by flag. Without
|
|
390
|
+
that distinction a consumer cannot tell "step 7 never ran" from "we dropped
|
|
391
|
+
step 7's screenshot".
|
|
392
|
+
- New optional extra: `pip install 'steppy-dev[bundle]'` adds `zstandard` for
|
|
393
|
+
`.tar.zst`. Without it bundles fall back to `.tar.gz` automatically — the free
|
|
394
|
+
path must not depend on an extra — though an explicitly named `.tar.zst` output
|
|
395
|
+
errors rather than silently producing gzip content.
|
|
396
|
+
- **Per-step events, so a bundle names the step a test died in.**
|
|
397
|
+
`manifest.tests[].failed_step` was declared from the start and could never be
|
|
398
|
+
populated: the event stream was test-granular, and a consumer's only fallback
|
|
399
|
+
was "steps with a screenshot were reached" — a guess that breaks as soon as
|
|
400
|
+
screenshots are off. Every generated step body is now bracketed by
|
|
401
|
+
`_steppy_mark_step` / `_steppy_end_step`, gated on `STEPPY_STEP_EVENTS=1`
|
|
402
|
+
(which `--bundle` sets; export `STEPPY_STEP_EVENTS=0` to opt out). The
|
|
403
|
+
markers cost one env lookup each when the gate is off, and they are emitted
|
|
404
|
+
unconditionally so a generated file never depends on the environment it was
|
|
405
|
+
generated in.
|
|
406
|
+
- **The unclosed marker is the answer**, rather than "the last step
|
|
407
|
+
reached". A step that raises never reaches its `_steppy_end_step`, so it
|
|
408
|
+
stays open; a test that dies in fixture teardown closed every step it ran
|
|
409
|
+
and correctly reports no failed step at all. The number reported is the
|
|
410
|
+
scenario's own step number, so steps skipped by `only-in:`/`layouts:`
|
|
411
|
+
cannot shift it — their markers are inside the guard and never run.
|
|
412
|
+
- Bracketing is a `bracket_step` filter in `generator.py`, **not** two lines
|
|
413
|
+
in the template, because whether a captured step body ends with a newline
|
|
414
|
+
turns out to depend on which action branch rendered — a branch whose last
|
|
415
|
+
literal is followed by `{%- elif %}` has it stripped when the template is
|
|
416
|
+
lexed. A template-level marker therefore glued onto the previous statement
|
|
417
|
+
for `wait` and nothing else, a `SyntaxError` in exactly the scenarios that
|
|
418
|
+
used it. `tests/test_step_events_codegen.py` now brackets every action in
|
|
419
|
+
`VALID_ACTIONS`.
|
|
420
|
+
- Regenerating adds two lines per step to every `test_*.py`. Runtime
|
|
421
|
+
behaviour is unchanged with the gate off.
|
|
422
|
+
- **Video guide mode** (`modes: ['video']` + `--video` on `steppy guide` and
|
|
423
|
+
`steppy build`). A third guide mode that *synthesizes* narrated walkthrough
|
|
424
|
+
videos from artifacts a run already captured — one MP4 per (guide × locale ×
|
|
425
|
+
layout), on **both** build targets. Nothing is screen-recorded: a new
|
|
426
|
+
`packages/player/src/video/` module lays the interactive manifest onto an
|
|
427
|
+
explicit millisecond timeline whose state at any instant is a pure function of
|
|
428
|
+
its inputs, and `steppy/video.py` steps that timeline frame by frame in
|
|
429
|
+
headless Chromium, piping stills to ffmpeg. So a guide renders identically on
|
|
430
|
+
a laptop and on a throttled CI runner, which is what makes "regenerated on
|
|
431
|
+
every release" a claim the tool can cash. Frames that change nothing (holds,
|
|
432
|
+
voiceover pauses — most of a narrated video) reuse the previous still instead
|
|
433
|
+
of re-screenshotting, and whole renders are content-addressed in
|
|
434
|
+
`.steppy/video-cache/`, so an unchanged guide costs nothing on the next build.
|
|
435
|
+
Each MP4 ships with a WebVTT caption track and a poster frame; the static
|
|
436
|
+
target emits a `<slug>/video.html` player page and Angular a
|
|
437
|
+
`<slug>-video.component.ts` wired into the existing mode toggle.
|
|
438
|
+
- **Narration is dependency-injected from day one.** Voiceover text is the
|
|
439
|
+
per-locale `guide-text` already authored — no new authoring surface — and
|
|
440
|
+
the engine is chosen entirely in `config.yaml`. The default `none` gives
|
|
441
|
+
burned-in captions with no install, no network and no bill; `piper` (free,
|
|
442
|
+
offline, neural) and `espeak` run as subprocesses; ElevenLabs, OpenAI, Azure
|
|
443
|
+
and Google are *data rows* over one generic HTTP provider rather than a
|
|
444
|
+
class each, so an unsupported vendor needs only `video.tts.http` filled in,
|
|
445
|
+
and a third party can register one through a `steppy.tts_providers` entry
|
|
446
|
+
point. No code path branches on vendor. Clips are cached by content (keyed
|
|
447
|
+
including the engine version, since a local engine's output shifts between
|
|
448
|
+
builds) so a paid provider is never re-billed for unchanged text, and each
|
|
449
|
+
step is stretched to fit its voiceover so narration is never cut off.
|
|
450
|
+
- Piper is invoked as a subprocess and never imported: piper1-gpl is GPL-3.0,
|
|
451
|
+
so it stays a user-installed tool rather than a dependency of a
|
|
452
|
+
permissively-licensed wheel — the same posture already taken with ffmpeg.
|
|
453
|
+
- Two gates by design: the mode is the content decision, `--video` the cost
|
|
454
|
+
one. Declaring the mode without the flag is inert; passing the flag with no
|
|
455
|
+
declaring guide warns and does nothing. New `steppy video check` verifies
|
|
456
|
+
ffmpeg, Chromium, the provider and the API key in about five seconds, rather
|
|
457
|
+
than letting each fail separately partway through a long render.
|
|
458
|
+
- Fixed in passing: `interactive.html` was emitted and listed in `sitemap.xml`
|
|
459
|
+
but linked from nowhere. Static guide pages now carry a mode-links row
|
|
460
|
+
surfacing whichever of the interactive tour and the video the guide declares.
|
|
461
|
+
- **PDF export** (`--pdf` / `--handbook` on `steppy guide` and `steppy build`).
|
|
462
|
+
`--pdf` prints every generated guide page through headless Chromium — the
|
|
463
|
+
templates' existing `@media print` rules are the PDF layout, so a themed
|
|
464
|
+
project's PDFs inherit its theme. `--handbook` assembles every guide into one
|
|
465
|
+
document (cover, contents, each guide on a fresh page) as HTML **and** PDF:
|
|
466
|
+
per (role, locale, layout) for `steppy guide`, per locale for `steppy build`,
|
|
467
|
+
where it is linked from the index and each page gains a "PDF" link. Paper
|
|
468
|
+
settings live in a new `pdf:` config block (format, margins, scale,
|
|
469
|
+
backgrounds, footer page numbers). Static build target only; a dry run never
|
|
470
|
+
launches a browser. Print CSS gained a `--steppy-print-max-image-height` cap
|
|
471
|
+
(Chromium can't split an image, so a tall full-page capture used to be clipped
|
|
472
|
+
at the page break) and hides the site chrome, which also fixes Ctrl+P on a
|
|
473
|
+
published guide site.
|
|
474
|
+
- **Scenario-level `layouts:` now narrows `steppy build` too.** The frontmatter
|
|
475
|
+
declaration already scoped capture; the build still rendered every configured
|
|
476
|
+
layout for every guide, so a desktop-only flow shipped a "Mobile" toggle that
|
|
477
|
+
swapped between identical screenshots. Each guide is now built for
|
|
478
|
+
`layouts ∩ scenario.layouts` — one wrapper, no toggle, one screenshot copy,
|
|
479
|
+
a single-layout interactive manifest — and `manifest.json` reports the
|
|
480
|
+
effective layouts per guide. A disjoint declaration is now a build error.
|
|
481
|
+
- **Parallel-run reliability & triage** — six changes aimed at making a
|
|
482
|
+
several-hundred-scenario `steppy run --workers N` produce a short, honest,
|
|
483
|
+
actionable summary:
|
|
484
|
+
- **Report flags now reach the workers.** `--trace`, `--video`, `--retries`
|
|
485
|
+
and the `--` pytest passthrough were silently dropped in parallel mode —
|
|
486
|
+
resolved host-side but never forwarded to the containers. They now travel as
|
|
487
|
+
`STEPPY_TRACE` / `STEPPY_VIDEO` / `STEPPY_RETRIES` / `STEPPY_PYTEST_EXTRA`.
|
|
488
|
+
The worker image bakes in the `[retry,report]` extras so `--reruns` actually
|
|
489
|
+
works in-container.
|
|
490
|
+
- **Retry pass + FLAKY classification** (`parallel.retry`, `--retry-pass N`).
|
|
491
|
+
After the queue drains, failed scenarios are re-run at reduced concurrency
|
|
492
|
+
(default: 1 worker) in the still-running stacks. Fail-then-pass is reported
|
|
493
|
+
as **FLAKY** in its own section and does **not** fail the build
|
|
494
|
+
(`--strict-flaky` opts into strict); fail-twice is a real failure. Orphaned
|
|
495
|
+
scenarios (worker died mid-run) are retried instead of being a hard loss.
|
|
496
|
+
Flaky scenarios are excluded from the `lastfailed` cache, so `steppy rerun`
|
|
497
|
+
only re-runs genuine failures.
|
|
498
|
+
- **Failures grouped by cause.** The summary leads with a *Failure causes*
|
|
499
|
+
block that buckets failures by a normalized fingerprint of their tail —
|
|
500
|
+
forty scenarios sharing one broken selector print as one line, not forty
|
|
501
|
+
stack traces. `--verbose` restores the full per-scenario dump.
|
|
502
|
+
- **Run history + `steppy flaky`.** Parallel runs append one record per
|
|
503
|
+
scenario to `.steppy/history.jsonl` (outside `output/`, so cleanup can't
|
|
504
|
+
wipe it). `steppy flaky` ranks repeat offenders by flake rate with
|
|
505
|
+
median/p95 durations; `--last`, `--min-rate`, `--json`, `--prune`.
|
|
506
|
+
- **Contention reduction.** `parallel.isolate_auth` gives each worker a
|
|
507
|
+
private, pre-seeded `.auth` dir instead of sharing one read-write across all
|
|
508
|
+
N (mutually exclusive with `pre_auth`). The queue now honours seed order and
|
|
509
|
+
the coordinator seeds longest-first from recorded durations, so a slow
|
|
510
|
+
scenario is no longer claimed last and left running alone.
|
|
511
|
+
- **Circuit breaker** (`parallel.circuit_breaker`, `--fail-fast/--no-fail-fast`).
|
|
512
|
+
Aborts a run once ≥10 scenarios have completed and ≥90% of them failed —
|
|
513
|
+
that is infrastructure, not tests, and it should not cost the full wall
|
|
514
|
+
clock. On by default; skips the retry pass when it trips.
|
|
515
|
+
- **`steppy update`** — reconciles a scaffolded project with the installed Steppy
|
|
516
|
+
version: refreshes Steppy-owned assets (config schema, Claude skills, CI workflow)
|
|
517
|
+
from the manifest — pristine files silently, user-edited files alongside as
|
|
518
|
+
`<file>.new` — then reports deprecated usage and changelog highlights. A skill
|
|
519
|
+
shipped after the project was scaffolded is installed (not into a project
|
|
520
|
+
scaffolded with `--no-skills`, nor one whose skills were all deleted), and the
|
|
521
|
+
skill folders people copied in by hand before Steppy packaged them (`steppy`,
|
|
522
|
+
`steppy-angular-export`) are reported, never removed. `steppy update --check` is
|
|
523
|
+
a report-only, CI-able compat scan (non-zero exit on breakage).
|
|
524
|
+
- **`steppy init`** — scaffolds a new project: a commented `config.yaml` (with a
|
|
525
|
+
`# yaml-language-server: $schema=…` header for editor autocomplete), a runnable
|
|
526
|
+
example scenario, `credentials.yaml.example`, a generated `steppy-config.schema.json`,
|
|
527
|
+
and a `.steppy/scaffold.json` manifest; appends the right `.gitignore` entries and
|
|
528
|
+
never overwrites existing files. `--ci` also writes a GitHub Actions workflow.
|
|
529
|
+
Re-running it is safe, and is how a project set up by hand adopts the scaffold:
|
|
530
|
+
it merges the manifest rather than replacing it, and leaves the starter scenario
|
|
531
|
+
out when `scenarios/` already has some.
|
|
532
|
+
- **Config JSON Schema** — `steppy-config.schema.json`, generated from `SteppyConfig`
|
|
533
|
+
and drift-guarded by a test, turns `extra = "forbid"` from hostile into helpful
|
|
534
|
+
(inline autocomplete + typo squiggles in YAML-aware editors).
|
|
535
|
+
- **CI reporting** — a `report:` config block (JUnit XML, HTML report via the
|
|
536
|
+
`steppy-dev[report]` extra, Playwright trace) and `steppy run --trace`; a bundled
|
|
537
|
+
GitHub Actions template.
|
|
538
|
+
- **Auth** — `form` auth mode (a rename of `auth0`, which works against any
|
|
539
|
+
fillable login form); IdP selector presets (`auth0`, `keycloak`); implemented
|
|
540
|
+
`token` `header` injection.
|
|
541
|
+
- **Parallel pre-authentication** — an optional `parallel.pre_auth` block (`command`,
|
|
542
|
+
`timeout`, `image`) runs a login command **in-network** before `steppy run --workers N`
|
|
543
|
+
dispatches, gated (Compose `service_completed_successfully`) to finish before any worker
|
|
544
|
+
claims a scenario and deduped once per run across stacks (O_EXCL lock). Fixes
|
|
545
|
+
`mode: storage-state` auth in worker mode, where a host-captured, origin-bound
|
|
546
|
+
`.auth/<role>.json` can't authenticate a worker reaching the app at its `internal_url`.
|
|
547
|
+
Unset ⇒ dispatch is byte-identical to before.
|
|
548
|
+
- **Selector lint** — `steppy validate` warns on brittle selectors (positional
|
|
549
|
+
pseudo-classes, deep chains, text-coupled, and structure-only tag chains) across
|
|
550
|
+
every author-written CSS surface (`selector`, `within`, `guide-cursor.target`, and
|
|
551
|
+
`capture` `from.selector`), with combinator-aware (`>`/`+`/`~`) hop counting —
|
|
552
|
+
without failing.
|
|
553
|
+
- **Scenario-format version** — optional `steppy:` frontmatter key with a supported-
|
|
554
|
+
version check, so future format changes can be detected rather than mistaken for typos.
|
|
555
|
+
- **Security** — `${VAR}` config interpolation, credential-hygiene warnings, opt-in
|
|
556
|
+
shell capture, screenshot redaction (`screenshot.mask`); see `SECURITY.md`.
|
|
557
|
+
- **UI-kit presets** — `preset: primeng | material | primevue | mui | vuetify | shadcn`
|
|
558
|
+
for the component-driver blocks. `primeng` is authoritative; the rest are best-effort
|
|
559
|
+
starter kits to refine against real markup.
|
|
560
|
+
- **React/Vue guide embedding** — `@steppy/react` and `@steppy/vue`, thin starter wrapper
|
|
561
|
+
packages around the framework-agnostic `<steppy-guide>` web component (props →
|
|
562
|
+
attributes, `steppy:*` events → callbacks/emits). Pair them with the `static` build
|
|
563
|
+
target to embed interactive guides in any React/Vue app.
|
|
564
|
+
- **`steppy record`** — a click-to-Markdown recorder: wraps `playwright codegen` and
|
|
565
|
+
post-processes its output into a **draft** `scenarios/<slug>.md`. Never bakes secrets —
|
|
566
|
+
a username/password/submit sequence collapses to `- action: login`, password/token
|
|
567
|
+
fields become `${VAR}` refs (resolved at run time by a new `_interp_val` conftest
|
|
568
|
+
helper) and drive an auto-suggested `input[type='password']` screenshot mask; the
|
|
569
|
+
selector lint runs on the output. Loads `.auth/<role>.json` for an authenticated
|
|
570
|
+
session. A foundational V1 (CLI plumbing) — the client-facing recorder is a later phase.
|
|
571
|
+
- **Docs** — `STEPPY-REFERENCE.md` (the authoritative scenario-format spec),
|
|
572
|
+
`SECURITY.md`, `CONTRIBUTING.md`, this `CHANGELOG.md`; README Troubleshooting,
|
|
573
|
+
Continuous integration, and Real-world scenarios sections.
|
|
574
|
+
- **Example scenarios** — the in-repo `scenarios/` set now covers every action
|
|
575
|
+
family and is validated by a CI test so it can't rot.
|
|
576
|
+
- **Opt-in video recording** — `report.video` (`off`/`on`/`on-failure`)
|
|
577
|
+
and `steppy run --video` record a Playwright video, saving `output/videos/<test>.webm`
|
|
578
|
+
(retain-on-failure by default). Off by default ⇒ `run` output byte-identical.
|
|
579
|
+
- **Flaky-retry** — `report.retries: N` (or `steppy run --retries N`) reruns
|
|
580
|
+
each failed test up to N times via the new `steppy-dev[retry]` extra (pytest-rerunfailures);
|
|
581
|
+
warns and skips when the plugin is absent, never fails the run.
|
|
582
|
+
- **TOTP/MFA login** — automated multi-factor login: a `totp_secret`
|
|
583
|
+
(env-ref) on app credentials + a `totp_selector` on `auth.form` (with `auth0`/`keycloak`
|
|
584
|
+
preset starters) fills a time-based one-time code after the password submit. Implemented
|
|
585
|
+
with a stdlib RFC 6238 helper — **no new dependency**, and the generated conftest stays
|
|
586
|
+
self-contained (consumers need nothing extra).
|
|
587
|
+
- **Brand.** A mark (a checked box) and an app icon in `docs/brand/`, with the
|
|
588
|
+
palette Pin / Lichen / Balise / Granite. The run report carries the mark, a
|
|
589
|
+
favicon and the palette. The generated guide sites keep their neutral styling
|
|
590
|
+
on purpose, because they document the user's application, not Steppy.
|
|
591
|
+
|
|
592
|
+
### Changed
|
|
593
|
+
- **The Python package is now named `steppy-dev`.** `steppy` on PyPI is an
|
|
594
|
+
unrelated project, so pip needs another name; the command, the import and
|
|
595
|
+
everything else stay `steppy`. Every install line and extras hint now says
|
|
596
|
+
`steppy-dev` (`pip install 'steppy-dev[report]'`, `steppy-dev @ git+…`),
|
|
597
|
+
including the CI workflows `steppy init --ci` scaffolds, which `steppy update`
|
|
598
|
+
refreshes. **If you installed an earlier build**, run `pip uninstall steppy`
|
|
599
|
+
before installing this one, since both claim the `steppy` folder and command.
|
|
600
|
+
Also rename the package in any requirement line that points at the git
|
|
601
|
+
repository: pip refuses a `@ git+…` line whose name does not match the package
|
|
602
|
+
it builds.
|
|
603
|
+
- **The guide player and video renderer bundles were rebuilt.** They now share
|
|
604
|
+
their tokens, strings, Markdown renderer and tooltip placement with the in-app
|
|
605
|
+
tour overlay (`packages/player/src/ui/`). The guide's styles are byte-identical;
|
|
606
|
+
cached videos re-render once, because the renderer's hash is part of the cache
|
|
607
|
+
key. A regional locale now falls back to its base language for the player's
|
|
608
|
+
chrome strings (`fr-BE` reads the French ones; it used to read English).
|
|
609
|
+
- **`STEPPY_RUN_ID` is now sortable in parallel mode too.** The coordinator used to
|
|
610
|
+
export its Compose-project id (a bare `uuid4().hex[:8]`) as the run identity, but
|
|
611
|
+
`coverage.merge.latest_run_id` resolves "the newest recorded run" by taking the
|
|
612
|
+
lexicographically last directory name — correct for the timestamped id the
|
|
613
|
+
single-worker path mints, arbitrary for a random one. The two are now separate:
|
|
614
|
+
a short `stack_id` names the Compose projects and the tmp dir, and the run's
|
|
615
|
+
identity is the same sortable `<timestamp>-<hex>` form everywhere. **Visible
|
|
616
|
+
effect:** under `--workers N --coverage`, `output/coverage/raw/<dir>` is named
|
|
617
|
+
differently than before, and `steppy coverage merge` with no `--run-id` now picks
|
|
618
|
+
the right one.
|
|
619
|
+
- **Default build target is now `static`** (was `angular`). The static site is
|
|
620
|
+
framework-agnostic and embeds anywhere, including via the new `@steppy/react` /
|
|
621
|
+
`@steppy/vue` wrapper packages. **Migration:** projects that build Angular components
|
|
622
|
+
must now set `target: 'angular'` explicitly in `steppy.guide.config.ts` — a build
|
|
623
|
+
config that omitted `target` and relied on the Angular default will otherwise emit a
|
|
624
|
+
static site.
|
|
625
|
+
- **Auth mode `auth0` → `form`** and **step action `auth0-login` → `login`** — the
|
|
626
|
+
old spellings remain kept-forever deprecated aliases (see `src/steppy/compat.py`).
|
|
627
|
+
- **Template tree** — the bundled `src/steppy/templates/` is now generated from the
|
|
628
|
+
canonical root `templates/` at build time; edit only the root tree.
|
|
629
|
+
- **Internal maintainability** (output byte-identical) — the screenshot-emit
|
|
630
|
+
predicate that was copy-pasted 36× in `test_scenario.py.j2` is now one registered
|
|
631
|
+
`emits_screenshot` Jinja filter; the scenario/locale/layout selection logic shared
|
|
632
|
+
by single-worker and parallel runs was extracted into `src/steppy/selection.py` so
|
|
633
|
+
`--workers N` can't drift from `--workers 1`; and the escaping-fuzz suite was
|
|
634
|
+
extended to frontmatter (title/description docstrings, seed path) and config layout
|
|
635
|
+
names, hardening those source-baked sites against quote/backslash injection.
|
|
636
|
+
- **De-branding** — CLI help, example config/scenarios, and fixtures use generic
|
|
637
|
+
`admin`/`editor`/`viewer` roles.
|
|
638
|
+
- **Versions aligned** — the Python package, npm types package, and player now share
|
|
639
|
+
one version, guarded by a test and the release workflow.
|
|
640
|
+
|
|
641
|
+
### Fixed
|
|
642
|
+
- **Guide text no longer turns protocol-relative links into links.** The player's
|
|
643
|
+
Markdown renderer accepted `[label](//host/path)` and `[label](/\host)`, which
|
|
644
|
+
browsers resolve to another site; both now stay literal text, like any other
|
|
645
|
+
refused URL.
|
|
646
|
+
- **The release workflow now vendors `steppy-video.js`.** It copied only two of
|
|
647
|
+
the three player bundles, so a release wheel shipped whatever video renderer
|
|
648
|
+
happened to be committed; the wheel check now requires all three. Releases also
|
|
649
|
+
attach an sdist.
|
|
650
|
+
- `contracts/report.schema.json` regenerated. It was out of sync with the models
|
|
651
|
+
after a pydantic upgrade, so the drift guard failed.
|
|
652
|
+
- **A multi-role scenario ran once per *pairing* of its roles' locales and layouts,
|
|
653
|
+
and the mixed runs filed screenshots in the wrong directory.** The
|
|
654
|
+
`pytest_generate_tests` hook parametrized each role page fixture with a
|
|
655
|
+
separate call, and separate calls on independent argnames multiply. Under
|
|
656
|
+
`--layouts desktop,mobile`, a two-role scenario collected 4 items and a
|
|
657
|
+
three-role one 8. With `--locales fr,nl` as well, a two-role scenario collected
|
|
658
|
+
16, one page in French and the other in Dutch. The extra items were also wrong:
|
|
659
|
+
`CURRENT_LOCALE`/`CURRENT_LAYOUT` are one global that every role fixture
|
|
660
|
+
writes, so the last one set up decided the screenshot directory for a page
|
|
661
|
+
built at the other role's viewport. The artefact was a desktop capture under
|
|
662
|
+
`fr/mobile/` whose sidecar claimed `viewport: {390, 844}` beside a
|
|
663
|
+
`bbox.x` of 1435. All role pages now go under one `parametrize` call and
|
|
664
|
+
move in lockstep. A real suite under `--layouts desktop,mobile` went from 1118
|
|
665
|
+
collected items to 616, and single-role ids are byte-identical. As hardening,
|
|
666
|
+
the sidecar's `viewport` (and the in-viewport test in multi-frame fill-form)
|
|
667
|
+
now reads `page.viewport_size`, the space the bboxes were actually measured
|
|
668
|
+
in, and falls back to the layout lookup only for a context without a
|
|
669
|
+
viewport. *Behaviour change: multi-role tests collect fewer items, and
|
|
670
|
+
their node ids lose the repeat (`[fr-mobile]`, not `[fr-mobile-fr-mobile]`), so
|
|
671
|
+
a single-worker `lastfailed` cache written before the upgrade will not match
|
|
672
|
+
them on the first `steppy rerun`.*
|
|
673
|
+
- **Files under `output/coverage/` that were not raw records were silently dropped
|
|
674
|
+
from every bundle.** `classify._coverage_file` returned `None` for anything not
|
|
675
|
+
matching `raw/<run_id>/…`, and the walker reads `None` as "belongs to a different
|
|
676
|
+
run, skip entirely" — the reading it was written for. So `output/coverage/gaps.json`,
|
|
677
|
+
and any file dropped there by hand, vanished without a warning, contradicting
|
|
678
|
+
that module's own rule that carrying a file nobody labelled beats dropping one
|
|
679
|
+
nobody noticed. `None` is now reserved for the previous-run case, `gaps.json`
|
|
680
|
+
ships as the new `coverage_gaps` kind (never dropped by the size budget — it is
|
|
681
|
+
kilobytes, and the only way a detached bundle can report a real coverage
|
|
682
|
+
surface), and anything else lands as `other`. Found by writing the report that
|
|
683
|
+
needed to read it, which is what the results-bundle acceptance test is for.
|
|
684
|
+
- **A non-ASCII character in Steppy's own output could kill the command.** Steppy's
|
|
685
|
+
summaries use `→`, `×` and `…`; on a Windows console still running a legacy code
|
|
686
|
+
page `sys.stdout` is cp1252, and the first arrow raised `UnicodeEncodeError` from
|
|
687
|
+
inside Rich — so `steppy coverage merge` and every other command with an arrow in
|
|
688
|
+
it died at the moment it tried to report success, with a charmap traceback that
|
|
689
|
+
said nothing about the run. The shared console now asks for UTF-8 with
|
|
690
|
+
`errors="replace"`, guarded on every axis.
|
|
691
|
+
- **The interactive player's `sidebar` template no longer degrades with step
|
|
692
|
+
count.** The left rail had no height cap and its timeline no scroll, so rail
|
|
693
|
+
height was linear in the number of steps: at 19 steps — the longest guides in a
|
|
694
|
+
real project — a ~1200px rail stood beside a 394px screenshot, ~800px of dead
|
|
695
|
+
column, worsening with every step added. The rail is now capped and its
|
|
696
|
+
*timeline* is what scrolls, so the head (title, meta, switchers) and foot
|
|
697
|
+
(prev/next/counter) stay pinned however long the guide is. The load-bearing
|
|
698
|
+
line is `min-height: 0` on the timeline: a flex item's default `min-height:
|
|
699
|
+
auto` refuses to shrink below its content, so without it the cap is inert and
|
|
700
|
+
nothing scrolls. Sticky went on the **rail**, not the preview — a tall mobile
|
|
701
|
+
capture renders ~1363px, and sticking *that* would pin its top and put its
|
|
702
|
+
bottom permanently out of reach, whereas sticking the rail keeps navigation in
|
|
703
|
+
view while the image scrolls and is a no-op the rest of the time. The preview
|
|
704
|
+
is now centred in its grid area, which turns the remaining slack into padding
|
|
705
|
+
instead of a void below the image; this is safe for the cursor and hotspot
|
|
706
|
+
because they are positioned inside `.steppy-stage` and scaled from the image's
|
|
707
|
+
live rect, so they travel with it. Advancing scrolls the current row into view
|
|
708
|
+
(`block: 'nearest'`), which is required once the timeline scrolls and also
|
|
709
|
+
absorbs the reflow from the description being injected into only the current
|
|
710
|
+
row. Rail width, rail cap and sticky offset are now `--steppy-*` tokens: the
|
|
711
|
+
shadow DOM exposes no `part=`, so without them a host that cannot live with a
|
|
712
|
+
default has to wait for a release. The static `interactive.html` shell widened
|
|
713
|
+
from `1024px` to `min(1440px, 100vw - 32px)` — at 1024px the sidebar spent 340
|
|
714
|
+
of it on the rail and rendered a 1440px capture at 44%; it now reaches ~73%,
|
|
715
|
+
and the overlay/inline stage goes from 990px to essentially native. That makes
|
|
716
|
+
the interactive page deliberately wider than the 900px editorial guide pages:
|
|
717
|
+
it is an app-like widget, not prose.
|
|
718
|
+
- **Interactive guides can express the desktop/mobile twin-branch pattern again,
|
|
719
|
+
and each layout now gets its own wording.** `prepare_interactive_manifest`
|
|
720
|
+
required every layout to cover the *same scenario step indices*, which
|
|
721
|
+
complementary `layouts:` whitelists — one logical step authored twice because
|
|
722
|
+
the DOM path differs — can never satisfy; it failed 18 of 24 guide-enabled
|
|
723
|
+
scenarios in a real project. Divergent indices are the pattern working as
|
|
724
|
+
intended; the merge is positional, and what the manifest genuinely cannot
|
|
725
|
+
express is a divergent step *count* per layout, which the pre-existing count
|
|
726
|
+
guard already caught and now reports with the per-layout coverage breakdown.
|
|
727
|
+
Underneath that, the merge had been taking `title`/`text`/`subtitle` from the
|
|
728
|
+
canonical (default-layout) branch alone, so mobile viewers read "click the row
|
|
729
|
+
of the table" beneath a screenshot of a card list — silently, in 34 of 199
|
|
730
|
+
steps in the same project. `captures[locale][layout]` now carries an optional
|
|
731
|
+
atomic `content: {title, text, subtitle}` override, emitted only where it
|
|
732
|
+
differs from the step level, so a guide with no twins stays byte-identical and
|
|
733
|
+
the video render cache does not churn. Manifest version `1.0` → `1.1` (the
|
|
734
|
+
player gates on the major, so old manifests still load — but `Capture` is a
|
|
735
|
+
strict schema, so emitter and vendored bundle must ship together). Twins that
|
|
736
|
+
disagree on `action` now warn instead of failing the build; the cursor-presence
|
|
737
|
+
check stays fatal, because that one really does change playback.
|
|
738
|
+
- **A killed video/narration render can no longer poison a cache permanently.**
|
|
739
|
+
ffmpeg wrote both the cached `.mp4` and the cached `.wav` *directly to the key
|
|
740
|
+
path*, and both lookups only asked `is_file()` — so a Ctrl-C, a full disk or an
|
|
741
|
+
encoder crash left a truncated artifact that every later run copied out and
|
|
742
|
+
reported as a hit, with `ok=True, cached=True`, forever. Every cache write now
|
|
743
|
+
stages a sibling temp file and `os.replace`s it into position. A hit is also
|
|
744
|
+
validated rather than trusted: a truncated WAV keeps its 44-byte header, so
|
|
745
|
+
`getnframes()` still reports the *intended* length and the timeline would be
|
|
746
|
+
sized against audio that is not there — the payload is now compared against the
|
|
747
|
+
bytes on disk, and a bad entry is discarded and re-made instead of raising a
|
|
748
|
+
bare `wave.Error` that killed the build.
|
|
749
|
+
- **Re-captured screenshots now invalidate the cached video.** The render
|
|
750
|
+
fingerprint hashed the manifest, which carries screenshot *URLs* — so a
|
|
751
|
+
recapture after a UI change that did not move the target element (a colour, a
|
|
752
|
+
logo, a label outside the bbox) produced the same key, and the build shipped a
|
|
753
|
+
video of the old UI and called it cached. Screenshot bytes are now hashed in,
|
|
754
|
+
memoized per batch so a guides × locales × layouts corpus reads each file once.
|
|
755
|
+
- **Setting an API key no longer re-encodes every video in the project.** The
|
|
756
|
+
fingerprint dumped the whole `VideoConfig`, nested `tts` block included, so
|
|
757
|
+
`api_key`, `timeout`, `ffmpeg_path` and `cache_dir` all invalidated renders
|
|
758
|
+
they cannot change a pixel of. They are excluded by an explicit denylist —
|
|
759
|
+
a field added later still invalidates by default.
|
|
760
|
+
- **Editing `video.tts.http` now re-synthesizes.** The HTTP engine's `version()`
|
|
761
|
+
returned only the model name, so changing the request body (ElevenLabs
|
|
762
|
+
`stability`/`style`, Azure SSML prosody, a different `format`) left every
|
|
763
|
+
existing clip a hit even though the audio would differ. It now folds in a hash
|
|
764
|
+
of the resolved spec — taken *pre-substitution*, so it carries `{api_key}`
|
|
765
|
+
placeholders rather than the secret and rotating a key does not churn the cache.
|
|
766
|
+
- **Re-wrapping a `guide-text` line is no longer a re-bill.** Narration text is
|
|
767
|
+
whitespace-normalized before it is sent and hashed, so a hard wrap moved by one
|
|
768
|
+
word costs nothing. (One-time full miss on the first render after upgrade.)
|
|
769
|
+
- **`video.tts.cache: false` no longer bills once per layout.** Narration does
|
|
770
|
+
not vary by layout, but each job re-synthesized from scratch with the on-disk
|
|
771
|
+
cache off; a batch now shares a process-local memo, so "do not persist" stops
|
|
772
|
+
meaning "pay N times".
|
|
773
|
+
- **`tts-cache/work/` no longer grows forever.** It kept every pre-transcode
|
|
774
|
+
container a provider ever returned; the normalized WAV is the artifact, so the
|
|
775
|
+
raw file is now deleted once it has been converted.
|
|
776
|
+
- A guide whose narration is empty now emits an empty WebVTT track instead of no
|
|
777
|
+
file at all — the embedding page advertises a `<track src=…>` decided at build
|
|
778
|
+
time, long before the render knows whether there is anything to say, so the
|
|
779
|
+
missing file was a 404.
|
|
780
|
+
- **`steppy-config.schema.json` no longer drifts on a pydantic upgrade.** The
|
|
781
|
+
committed schema is a generated artifact whose bytes are byte-compared against
|
|
782
|
+
the models by a guard test — but `pydantic>=2.0.0` is unbounded, and two of its
|
|
783
|
+
outputs vary across 2.x releases (`additionalProperties: true` on open dicts,
|
|
784
|
+
and `0` vs `0.0` in numeric bounds). Both leaked into the file, so the guard
|
|
785
|
+
failed for anyone whose pydantic differed from whoever last regenerated it,
|
|
786
|
+
reporting a model change that had not happened. The generator now canonicalises
|
|
787
|
+
those away, so the schema depends on the models and nothing else.
|
|
788
|
+
- Unescaped scenario strings (apostrophes, quotes) no longer break generated code.
|
|
789
|
+
- `steppy validate`/`list`/`generate` no longer report success as failure
|
|
790
|
+
(`typer.Exit` handling); unexpected errors show a friendly message, full traceback
|
|
791
|
+
only under `--verbose` / `STEPPY_DEBUG=1`.
|
|
792
|
+
- Malformed step lines and missing required fields now fail at `validate` time
|
|
793
|
+
instead of silently dropping intent.
|
|
794
|
+
|
|
795
|
+
### For contributors
|
|
796
|
+
- **Releases can publish to PyPI.** A `publish-pypi` job in `release.yml` uploads
|
|
797
|
+
the wheel and sdist the release job built, through Trusted Publishing, once the
|
|
798
|
+
`PYPI_PUBLISH` repository variable is `true`. The release job also runs
|
|
799
|
+
`twine check --strict` on them first. The one-time setup for PyPI and npm is in
|
|
800
|
+
CONTRIBUTING.md, "Releasing". `tests/test_distribution_name.py` fails when an
|
|
801
|
+
install hint names plain `steppy`.
|
|
802
|
+
- **In-app tours bring their own guards**: a stdlib-only-leaves check, the
|
|
803
|
+
tours lazy-import guard, Python↔TypeScript name sync, a shared golden case
|
|
804
|
+
table run by both the producer check and the overlay's validator, a docs sync
|
|
805
|
+
test, a vendored-bundle pin (`tests/test_player_bundle_files.py`), the overlay's
|
|
806
|
+
size budget in the `player` CI job, and a new `tour-e2e` CI job running the
|
|
807
|
+
Chromium suites. The step loop of `test_scenario.py.j2` moved into a
|
|
808
|
+
`step_body` macro — byte-identical output, proven by `tests/_codegen_snapshot.py`
|
|
809
|
+
— so the tour replay renders exactly the same action code.
|
|
810
|
+
- **The tour browser suites measure what the user sees.** Both demos gained a
|
|
811
|
+
phone layout (a menu drawer instead of the header links), a long page and a
|
|
812
|
+
confirmation, and every demo scenario runs at 1280x800 and 390x844, replays
|
|
813
|
+
included. `tests/integration/tour_geometry.py` checks each step in the real
|
|
814
|
+
browser: the spotlight frames the target, the bubble stays on screen and off
|
|
815
|
+
it, a click at the target reaches it, and no open dialog is covered — by mouse
|
|
816
|
+
on a computer and by touch on a phone, through the drawer, Escape in it, a
|
|
817
|
+
focus trap that pulls focus back, and a language switch.
|
|
818
|
+
- **The Angular adapter is tested three ways**: the CLI's Vitest builder runs a
|
|
819
|
+
zoneless suite and a zone.js suite (`npm --prefix packages/angular test`) against
|
|
820
|
+
a fake overlay and the real one; `tests/test_angular_adapter_sync.py` keeps its
|
|
821
|
+
tag, event names and property writes in step with the overlay; and the
|
|
822
|
+
`tour-angular-e2e` CI job runs the generated replays and the router, zone and
|
|
823
|
+
i18n checks against the Angular demo in Chromium
|
|
824
|
+
(`tests/integration/test_tour_angular_it.py`). A new `angular-adapter` job
|
|
825
|
+
builds the package and dry-runs its `npm pack`.
|
|
826
|
+
- **The lint gate is pinned, and now actually runnable.** `uv run ruff check .` —
|
|
827
|
+
the command CONTRIBUTING documents — failed with "program not found", so people
|
|
828
|
+
reached for whatever ruff was on PATH. That is not harmless: the formatter
|
|
829
|
+
changed how it wraps a long `assert x, "msg"` in 0.9, and an older ruff does not
|
|
830
|
+
skip the rule, it reformats the other way, quietly rewriting files the repo just
|
|
831
|
+
formatted. Ruff now resolves from a `dev` dependency group, `required-version`
|
|
832
|
+
refuses an incompatible one, and the lint rule set is **explicit** rather than
|
|
833
|
+
inherited — ruff 0.16 widened its defaults and took the unchanged tree from
|
|
834
|
+
clean to 745 errors on upgrade. Markdown is excluded from the formatter, which
|
|
835
|
+
0.16 would otherwise rewrite (the docs are full of deliberately-bad "BEFORE"
|
|
836
|
+
snippets that exist to be ugly).
|
|
837
|
+
- **`config.output.screenshots_dir` and `guides_dir` actually do something now**
|
|
838
|
+
They were validated, schema'd and scaffolded — and read by
|
|
839
|
+
nothing, so setting one changed exactly nothing while four independent copies
|
|
840
|
+
of the same hardcoded literal decided where artifacts really went. The capture
|
|
841
|
+
root joined the rest of the capture-path grammar in `captures.py`, and every
|
|
842
|
+
consumer resolves through it: the generated conftest (at *runtime*, like
|
|
843
|
+
`coverage.records_dir` — so editing `config.yaml` needs no re-`generate`, and
|
|
844
|
+
the generated file stays identical for every project), `steppy guide`'s two
|
|
845
|
+
path flags, `steppy build` (explicit `screenshotsDir` still wins), the
|
|
846
|
+
parallel failure harvest, and `steppy bundle create`. A project that sets
|
|
847
|
+
nothing keeps byte-identical paths.
|
|
848
|
+
|
|
849
|
+
Pointing a root outside `output/` is allowed and honoured, but three things
|
|
850
|
+
only ever see `output/` and each now says so: a bundle warns the artifacts are
|
|
851
|
+
not in the archive, `--workers N` warns only `output/` is mounted back from
|
|
852
|
+
the container, and the config comments name the CI upload and `.gitignore`.
|
|
853
|
+
|
|
854
|
+
`output.reports_dir` is **deprecated and inert** — nothing ever wrote
|
|
855
|
+
`output/reports` (traces and videos are top-level; `report.junit_xml` /
|
|
856
|
+
`report.html` carry their own paths). It still parses, so no existing config
|
|
857
|
+
stops loading; `steppy validate` warns and `steppy update --check` reports it.
|
|
858
|
+
`output:` also gained `extra: forbid`, so a `screenshot_dir:` typo now fails
|
|
859
|
+
loudly instead of being swallowed.
|
|
860
|
+
|
|
861
|
+
[Unreleased]: https://github.com/Steppy-dev/steppy/compare/v0.3.0...HEAD
|
|
862
|
+
[0.3.0]: https://github.com/Steppy-dev/steppy/releases/tag/v0.3.0
|