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
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// GENERATED BY STEPPY — DO NOT EDIT. Regenerate with: steppy build
|
|
2
|
+
import { Component, computed, effect, ElementRef, inject, signal, ViewEncapsulation } from '@angular/core';
|
|
3
|
+
import { DomSanitizer } from '@angular/platform-browser';
|
|
4
|
+
import { toSignal } from '@angular/core/rxjs-interop';
|
|
5
|
+
import { map, startWith } from 'rxjs/operators';
|
|
6
|
+
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
|
7
|
+
{% if has_multiple_modes or has_multiple_layouts %}
|
|
8
|
+
import { FormsModule } from '@angular/forms';
|
|
9
|
+
import { SelectButtonModule } from 'primeng/selectbutton';
|
|
10
|
+
{% endif %}
|
|
11
|
+
{% if onboarding_strip %}
|
|
12
|
+
import { RouterLink } from '@angular/router';
|
|
13
|
+
{% endif %}
|
|
14
|
+
{% if has_interactive %}
|
|
15
|
+
import { {{ class_name }}InteractiveComponent } from './{{ slug }}-interactive.component';
|
|
16
|
+
{% endif %}
|
|
17
|
+
{% if has_video %}
|
|
18
|
+
import { {{ class_name }}VideoComponent } from './{{ slug }}-video.component';
|
|
19
|
+
{% endif %}
|
|
20
|
+
import { CONTENT, STEPS, TIMINGS } from './{{ slug }}.content';
|
|
21
|
+
|
|
22
|
+
// Stepbystep cursor animation. The guide HTML is injected via [innerHTML], and
|
|
23
|
+
// the browser never runs <script> tags inserted that way — so the cursor
|
|
24
|
+
// driver is loaded here (once per page, like the interactive player bundle)
|
|
25
|
+
// and (re)invoked after every render. The vendored IIFE exposes
|
|
26
|
+
// window.__STEPPY_CURSOR__.mount(root, steps, timings), which returns a
|
|
27
|
+
// disposer that disconnects the observer and aborts any in-flight animation.
|
|
28
|
+
const CURSOR_BUNDLE_URL = '/assets/{{ role }}/steppy-cursor.js';
|
|
29
|
+
|
|
30
|
+
interface SteppyCursorApi {
|
|
31
|
+
mount(root: ParentNode, steps: unknown, timings?: unknown): () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let cursorScriptLoading: Promise<void> | null = null;
|
|
35
|
+
|
|
36
|
+
function ensureCursorLoaded(): Promise<void> {
|
|
37
|
+
// Idempotent: inserts the <script> once per page no matter how many guides
|
|
38
|
+
// mount. Resolves immediately once window.__STEPPY_CURSOR__ is available.
|
|
39
|
+
if (cursorScriptLoading) return cursorScriptLoading;
|
|
40
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
41
|
+
return Promise.resolve();
|
|
42
|
+
}
|
|
43
|
+
if ((window as unknown as { __STEPPY_CURSOR__?: SteppyCursorApi }).__STEPPY_CURSOR__) {
|
|
44
|
+
cursorScriptLoading = Promise.resolve();
|
|
45
|
+
return cursorScriptLoading;
|
|
46
|
+
}
|
|
47
|
+
cursorScriptLoading = new Promise<void>((resolve, reject) => {
|
|
48
|
+
const script = document.createElement('script');
|
|
49
|
+
script.src = CURSOR_BUNDLE_URL;
|
|
50
|
+
script.async = true;
|
|
51
|
+
script.onload = () => resolve();
|
|
52
|
+
script.onerror = (err) => reject(err);
|
|
53
|
+
document.head.appendChild(script);
|
|
54
|
+
});
|
|
55
|
+
return cursorScriptLoading;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function scheduleAfterRender(cb: () => void): void {
|
|
59
|
+
// Defer past Angular's [innerHTML] DOM flush so the freshly-injected
|
|
60
|
+
// .steppy-step sections exist before the driver observes them.
|
|
61
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
62
|
+
requestAnimationFrame(() => cb());
|
|
63
|
+
} else {
|
|
64
|
+
setTimeout(cb, 0);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@Component({
|
|
69
|
+
selector: 'app-guide-{{ slug }}',
|
|
70
|
+
standalone: true,
|
|
71
|
+
imports: [
|
|
72
|
+
TranslatePipe,
|
|
73
|
+
{% if has_multiple_modes or has_multiple_layouts %}
|
|
74
|
+
FormsModule,
|
|
75
|
+
SelectButtonModule,
|
|
76
|
+
{% endif %}
|
|
77
|
+
{% if onboarding_strip %}
|
|
78
|
+
RouterLink,
|
|
79
|
+
{% endif %}
|
|
80
|
+
{% if has_interactive %}
|
|
81
|
+
{{ class_name }}InteractiveComponent,
|
|
82
|
+
{% endif %}
|
|
83
|
+
{% if has_video %}
|
|
84
|
+
{{ class_name }}VideoComponent,
|
|
85
|
+
{% endif %}
|
|
86
|
+
],
|
|
87
|
+
template: `
|
|
88
|
+
{% if onboarding_strip %}
|
|
89
|
+
<nav class="steppy-onboarding-strip" aria-label="Onboarding progress">
|
|
90
|
+
<span class="steppy-onboarding-strip-progress">
|
|
91
|
+
<a [routerLink]="['../', '{{ onboarding_strip.path_slug }}']">{{ '{{' }} '{{ role_namespace }}.onboarding.title' | translate {{ '}}' }}</a>
|
|
92
|
+
— {{ '{{' }} '{{ role_namespace }}.onboarding.stepLabel' | translate: { position: {{ onboarding_strip.position }}, total: {{ onboarding_strip.total }} } {{ '}}' }}
|
|
93
|
+
</span>
|
|
94
|
+
<span class="steppy-onboarding-strip-nav">
|
|
95
|
+
{% if onboarding_strip.prev %}
|
|
96
|
+
<a class="steppy-onboarding-prev"
|
|
97
|
+
[routerLink]="['../', '{{ onboarding_strip.prev.slug }}']"
|
|
98
|
+
[queryParams]="{ onboarding: 1 }">‹ {{ '{{' }} '{{ role_namespace }}.guides.{{ onboarding_strip.prev.slug }}.title' | translate {{ '}}' }}</a>
|
|
99
|
+
{% else %}
|
|
100
|
+
<span class="steppy-onboarding-prev steppy-onboarding-disabled" aria-disabled="true">‹ {{ '{{' }} '{{ role_namespace }}.onboarding.previousLabel' | translate {{ '}}' }}</span>
|
|
101
|
+
{% endif %}
|
|
102
|
+
{% if onboarding_strip.next %}
|
|
103
|
+
<a class="steppy-onboarding-next"
|
|
104
|
+
[routerLink]="['../', '{{ onboarding_strip.next.slug }}']"
|
|
105
|
+
[queryParams]="{ onboarding: 1 }">{{ '{{' }} '{{ role_namespace }}.guides.{{ onboarding_strip.next.slug }}.title' | translate {{ '}}' }} ›</a>
|
|
106
|
+
{% else %}
|
|
107
|
+
<span class="steppy-onboarding-next steppy-onboarding-disabled" aria-disabled="true">{{ '{{' }} '{{ role_namespace }}.onboarding.doneLabel' | translate {{ '}}' }}</span>
|
|
108
|
+
{% endif %}
|
|
109
|
+
</span>
|
|
110
|
+
</nav>
|
|
111
|
+
{% endif %}
|
|
112
|
+
{% if has_multiple_modes or has_multiple_layouts %}
|
|
113
|
+
<div class="guide-toolbar">
|
|
114
|
+
{% if has_multiple_layouts %}
|
|
115
|
+
<p-selectButton
|
|
116
|
+
[options]="layoutOptions"
|
|
117
|
+
[ngModel]="selectedLayout()"
|
|
118
|
+
(ngModelChange)="selectedLayout.set($event)"
|
|
119
|
+
optionLabel="label"
|
|
120
|
+
optionValue="value" />
|
|
121
|
+
{% endif %}
|
|
122
|
+
{% if has_multiple_modes %}
|
|
123
|
+
<p-selectButton
|
|
124
|
+
[options]="modeOptions"
|
|
125
|
+
[(ngModel)]="selectedMode"
|
|
126
|
+
optionLabel="label"
|
|
127
|
+
optionValue="value" />
|
|
128
|
+
{% endif %}
|
|
129
|
+
</div>
|
|
130
|
+
{% endif %}
|
|
131
|
+
{% if has_interactive or has_video %}
|
|
132
|
+
@if (selectedMode() === 'stepbystep') {
|
|
133
|
+
<div [innerHTML]="trustedHtml()"></div>
|
|
134
|
+
}
|
|
135
|
+
{% if has_interactive %}
|
|
136
|
+
@if (selectedMode() === 'interactive') {
|
|
137
|
+
<app-guide-{{ slug }}-interactive{% if has_multiple_layouts %} [variant]="selectedLayout()"{% endif %} />
|
|
138
|
+
}
|
|
139
|
+
{% endif %}
|
|
140
|
+
{% if has_video %}
|
|
141
|
+
@if (selectedMode() === 'video') {
|
|
142
|
+
<app-guide-{{ slug }}-video{% if has_multiple_layouts %} [variant]="selectedLayout()"{% endif %} />
|
|
143
|
+
}
|
|
144
|
+
{% endif %}
|
|
145
|
+
{% else %}
|
|
146
|
+
<div [innerHTML]="trustedHtml()"></div>
|
|
147
|
+
{% endif %}
|
|
148
|
+
`,
|
|
149
|
+
encapsulation: ViewEncapsulation.None,
|
|
150
|
+
})
|
|
151
|
+
export class {{ class_name }}Component {
|
|
152
|
+
private readonly translate = inject(TranslateService);
|
|
153
|
+
private readonly sanitizer = inject(DomSanitizer);
|
|
154
|
+
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);
|
|
155
|
+
|
|
156
|
+
{% if has_multiple_layouts %}
|
|
157
|
+
readonly layoutOptions = [
|
|
158
|
+
{% for layout in layouts %}
|
|
159
|
+
{ label: '{{ layout | capitalize }}', value: '{{ layout }}' },
|
|
160
|
+
{% endfor %}
|
|
161
|
+
];
|
|
162
|
+
{% endif %}
|
|
163
|
+
|
|
164
|
+
{% if has_multiple_modes %}
|
|
165
|
+
readonly modeOptions = [
|
|
166
|
+
{% for mode in modes %}
|
|
167
|
+
{ label: '{{ mode_labels[mode] }}', value: '{{ mode }}' },
|
|
168
|
+
{% endfor %}
|
|
169
|
+
];
|
|
170
|
+
{% endif %}
|
|
171
|
+
|
|
172
|
+
readonly selectedLocale = toSignal(
|
|
173
|
+
this.translate.onLangChange.pipe(
|
|
174
|
+
map((event) => event.lang),
|
|
175
|
+
startWith(this.translate.currentLang || this.translate.getDefaultLang() || '{{ default_locale }}'),
|
|
176
|
+
),
|
|
177
|
+
{ initialValue: this.translate.currentLang || this.translate.getDefaultLang() || '{{ default_locale }}' },
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
readonly selectedLayout = signal<string>('{{ default_layout }}');
|
|
181
|
+
{% if has_multiple_modes %}
|
|
182
|
+
selectedMode = signal('{{ default_mode }}');
|
|
183
|
+
{% else %}
|
|
184
|
+
selectedMode = signal('stepbystep');
|
|
185
|
+
{% endif %}
|
|
186
|
+
|
|
187
|
+
readonly trustedHtml = computed(() => {
|
|
188
|
+
const byLayout = CONTENT[this.selectedLocale()] ?? {};
|
|
189
|
+
const layoutKey = this.selectedLayout();
|
|
190
|
+
// Fall back to "default" (the back-compat single-layout key) if the
|
|
191
|
+
// requested layout isn't present — handles cases where the consumer
|
|
192
|
+
// toggles to a layout that this guide didn't capture.
|
|
193
|
+
const raw = byLayout[layoutKey] ?? byLayout['default'] ?? '';
|
|
194
|
+
return this.sanitizer.bypassSecurityTrustHtml(raw);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Stepbystep cursor lifecycle. Re-drives the animator whenever the rendered
|
|
198
|
+
// DOM changes — locale, layout, and the mode toggle each swap the
|
|
199
|
+
// [innerHTML] content under our feet. The effect's cleanup disposes the
|
|
200
|
+
// previous mount before the next one (and on destroy).
|
|
201
|
+
private cursorDispose: (() => void) | null = null;
|
|
202
|
+
|
|
203
|
+
constructor() {
|
|
204
|
+
effect((onCleanup) => {
|
|
205
|
+
// Track the signals that change the injected DOM so this re-runs on each.
|
|
206
|
+
const locale = this.selectedLocale();
|
|
207
|
+
const layout = this.selectedLayout();
|
|
208
|
+
const mode = this.selectedMode();
|
|
209
|
+
if (mode !== 'stepbystep' || typeof window === 'undefined') return;
|
|
210
|
+
let cancelled = false;
|
|
211
|
+
onCleanup(() => {
|
|
212
|
+
cancelled = true;
|
|
213
|
+
this.cursorDispose?.();
|
|
214
|
+
this.cursorDispose = null;
|
|
215
|
+
});
|
|
216
|
+
scheduleAfterRender(() => {
|
|
217
|
+
if (cancelled) return;
|
|
218
|
+
const byLayout = STEPS[locale] ?? {};
|
|
219
|
+
const steps = byLayout[layout] ?? byLayout['default'] ?? [];
|
|
220
|
+
void ensureCursorLoaded().then(() => {
|
|
221
|
+
if (cancelled) return;
|
|
222
|
+
const api = (window as unknown as { __STEPPY_CURSOR__?: SteppyCursorApi }).__STEPPY_CURSOR__;
|
|
223
|
+
if (!api) return;
|
|
224
|
+
this.cursorDispose = api.mount(this.host.nativeElement, steps, TIMINGS);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// GENERATED BY STEPPY — DO NOT EDIT. Regenerate with: steppy build
|
|
2
|
+
export const CONTENT: Record<string, Record<string, string>> = {
|
|
3
|
+
{% for locale, by_layout in content_by_locale.items() %}
|
|
4
|
+
'{{ locale }}': {
|
|
5
|
+
{% for layout_key, html in by_layout.items() %}
|
|
6
|
+
'{{ layout_key }}': {{ html | tojson }},
|
|
7
|
+
{% endfor %}
|
|
8
|
+
},
|
|
9
|
+
{% endfor %}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Per-step cursor frame data, keyed by locale then layout (mirroring CONTENT;
|
|
13
|
+
// single-layout configs collapse to a single "default" key). The stepbystep
|
|
14
|
+
// cursor driver (window.__STEPPY_CURSOR__.mount) reads this to animate the
|
|
15
|
+
// arrow over the [innerHTML]-injected guide: each frame carries the
|
|
16
|
+
// capture-space viewport + cursor behavior the animator projects onto the
|
|
17
|
+
// displayed screenshot. `index` matches the `data-step-index` on each
|
|
18
|
+
// `.steppy-step` section.
|
|
19
|
+
export interface SteppyCursorStepFrame {
|
|
20
|
+
viewport: { width: number; height: number } | null;
|
|
21
|
+
cursor: unknown;
|
|
22
|
+
}
|
|
23
|
+
export interface SteppyCursorStep {
|
|
24
|
+
index: number;
|
|
25
|
+
frames: SteppyCursorStepFrame[];
|
|
26
|
+
}
|
|
27
|
+
export const STEPS: Record<string, Record<string, SteppyCursorStep[]>> = {{ steps_by_locale | tojson }};
|
|
28
|
+
|
|
29
|
+
// Cursor timing overrides for this guide (scenario.guide.cursor); passed
|
|
30
|
+
// straight to the animator, which fills any gaps with its defaults.
|
|
31
|
+
export const TIMINGS: Record<string, number> = {{ cursor_timings | tojson }};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// GENERATED BY STEPPY — DO NOT EDIT. Regenerate with: steppy build
|
|
2
|
+
import { Component, ViewEncapsulation } from '@angular/core';
|
|
3
|
+
import { RouterLink } from '@angular/router';
|
|
4
|
+
import { TranslatePipe } from '@ngx-translate/core';
|
|
5
|
+
|
|
6
|
+
interface GroupChild {
|
|
7
|
+
slug: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@Component({
|
|
11
|
+
selector: 'app-guide-group-{{ group.slug }}',
|
|
12
|
+
standalone: true,
|
|
13
|
+
imports: [RouterLink, TranslatePipe],
|
|
14
|
+
template: `
|
|
15
|
+
<section class="steppy-index">
|
|
16
|
+
<h1>{{ '{{' }} '{{ role_namespace }}.groups.{{ group.slug }}.title' | translate {{ '}}' }}</h1>
|
|
17
|
+
{% if has_description %}
|
|
18
|
+
<p class="steppy-group-description">{{ '{{' }} '{{ role_namespace }}.groups.{{ group.slug }}.description' | translate {{ '}}' }}</p>
|
|
19
|
+
{% endif %}
|
|
20
|
+
@if (children.length === 0) {
|
|
21
|
+
<p class="steppy-index-empty">{{ '{{' }} '{{ role_namespace }}.ui.groupEmpty' | translate {{ '}}' }}</p>
|
|
22
|
+
} @else {
|
|
23
|
+
<ul class="steppy-index-list">
|
|
24
|
+
@for (child of children; track child.slug) {
|
|
25
|
+
<li>
|
|
26
|
+
<a [routerLink]="['../', child.slug]">
|
|
27
|
+
<span class="steppy-index-title">{{ '{{' }} '{{ role_namespace }}.guides.' + child.slug + '.title' | translate {{ '}}' }}</span>
|
|
28
|
+
</a>
|
|
29
|
+
</li>
|
|
30
|
+
}
|
|
31
|
+
</ul>
|
|
32
|
+
}
|
|
33
|
+
</section>
|
|
34
|
+
`,
|
|
35
|
+
encapsulation: ViewEncapsulation.None,
|
|
36
|
+
})
|
|
37
|
+
export class {{ class_name }}GroupComponent {
|
|
38
|
+
readonly children: GroupChild[] = {{ children | tojson(indent=2) }};
|
|
39
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// GENERATED BY STEPPY — DO NOT EDIT. Regenerate with: steppy build
|
|
2
|
+
import { Component, ViewEncapsulation } from '@angular/core';
|
|
3
|
+
import { RouterLink } from '@angular/router';
|
|
4
|
+
import { TranslatePipe } from '@ngx-translate/core';
|
|
5
|
+
|
|
6
|
+
interface OnboardingStep {
|
|
7
|
+
slug: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@Component({
|
|
11
|
+
selector: 'app-guide-onboarding',
|
|
12
|
+
standalone: true,
|
|
13
|
+
imports: [RouterLink, TranslatePipe],
|
|
14
|
+
template: `
|
|
15
|
+
<section class="steppy-onboarding">
|
|
16
|
+
<h1>{{ '{{' }} '{{ role_namespace }}.onboarding.title' | translate {{ '}}' }}</h1>
|
|
17
|
+
{% if has_description %}
|
|
18
|
+
<p class="steppy-onboarding-description">{{ '{{' }} '{{ role_namespace }}.onboarding.description' | translate {{ '}}' }}</p>
|
|
19
|
+
{% endif %}
|
|
20
|
+
<ol class="steppy-onboarding-steps">
|
|
21
|
+
@for (step of steps; track step.slug; let i = $index) {
|
|
22
|
+
<li>
|
|
23
|
+
<a [routerLink]="['../', step.slug]" [queryParams]="{ onboarding: 1 }">
|
|
24
|
+
<span class="steppy-onboarding-step-number">{{ '{{' }} i + 1 {{ '}}' }}</span>
|
|
25
|
+
<span class="steppy-onboarding-step-title">{{ '{{' }} '{{ role_namespace }}.guides.' + step.slug + '.title' | translate {{ '}}' }}</span>
|
|
26
|
+
</a>
|
|
27
|
+
</li>
|
|
28
|
+
}
|
|
29
|
+
</ol>
|
|
30
|
+
@if (steps.length > 0) {
|
|
31
|
+
<p class="steppy-onboarding-cta">
|
|
32
|
+
<a class="steppy-onboarding-start"
|
|
33
|
+
[routerLink]="['../', steps[0].slug]"
|
|
34
|
+
[queryParams]="{ onboarding: 1 }">{{ '{{' }} '{{ role_namespace }}.onboarding.start' | translate {{ '}}' }}</a>
|
|
35
|
+
</p>
|
|
36
|
+
}
|
|
37
|
+
</section>
|
|
38
|
+
`,
|
|
39
|
+
encapsulation: ViewEncapsulation.None,
|
|
40
|
+
})
|
|
41
|
+
export class OnboardingComponent {
|
|
42
|
+
readonly steps: OnboardingStep[] = {{ steps | tojson(indent=2) }};
|
|
43
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// GENERATED BY STEPPY — DO NOT EDIT. Regenerate with: steppy build
|
|
2
|
+
// Role: {{ role }}
|
|
3
|
+
import { Routes } from '@angular/router';
|
|
4
|
+
|
|
5
|
+
export const {{ routes_const }}: Routes = [
|
|
6
|
+
{% for guide in guides %}
|
|
7
|
+
{
|
|
8
|
+
path: '{{ guide.slug }}',
|
|
9
|
+
loadComponent: () =>
|
|
10
|
+
import('./{{ guide.slug }}/{{ guide.slug }}.component').then(
|
|
11
|
+
(m) => m.{{ guide.class_name }}Component,
|
|
12
|
+
),
|
|
13
|
+
},
|
|
14
|
+
{% endfor %}
|
|
15
|
+
{% for group in groups %}
|
|
16
|
+
{
|
|
17
|
+
path: '{{ group.slug }}',
|
|
18
|
+
loadComponent: () =>
|
|
19
|
+
import('./_groups/{{ group.slug }}.group.component').then(
|
|
20
|
+
(m) => m.{{ group.class_name }}GroupComponent,
|
|
21
|
+
),
|
|
22
|
+
},
|
|
23
|
+
{% endfor %}
|
|
24
|
+
{% if has_onboarding %}
|
|
25
|
+
{
|
|
26
|
+
path: '{{ onboarding_slug }}',
|
|
27
|
+
loadComponent: () =>
|
|
28
|
+
import('./_onboarding/onboarding.component').then(
|
|
29
|
+
(m) => m.OnboardingComponent,
|
|
30
|
+
),
|
|
31
|
+
},
|
|
32
|
+
{% endif %}
|
|
33
|
+
{ path: '', redirectTo: '{{ default_slug }}', pathMatch: 'full' },
|
|
34
|
+
];
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Steppy CI workflow — copy this to .github/workflows/steppy.yml in your project.
|
|
2
|
+
#
|
|
3
|
+
# Brings your application stack up with Docker Compose, runs your Steppy
|
|
4
|
+
# scenarios against it, and publishes a browsable run report as a build artifact.
|
|
5
|
+
#
|
|
6
|
+
# Prerequisites in your repo:
|
|
7
|
+
# - config.yaml + scenarios/ committed
|
|
8
|
+
# - a docker-compose.yml that boots your app (see the two notes on the compose
|
|
9
|
+
# step below — healthchecks and published ports are both load-bearing)
|
|
10
|
+
# - credentials provided via repository secrets (referenced as ${VAR} in
|
|
11
|
+
# config.yaml — e.g. STEPPY_ADMIN_USERNAME / STEPPY_ADMIN_PASSWORD)
|
|
12
|
+
#
|
|
13
|
+
# If your app already runs somewhere the runner can reach, drop the compose steps
|
|
14
|
+
# and point apps.<role>.url at that deployment instead.
|
|
15
|
+
#
|
|
16
|
+
# This file is the OWNED copy: every trap is documented beside the step it applies
|
|
17
|
+
# to, and it is yours to edit. If you would rather not own it, Steppy also ships
|
|
18
|
+
# the same recipe as a reusable workflow — pin a tag, never a branch:
|
|
19
|
+
#
|
|
20
|
+
# jobs:
|
|
21
|
+
# e2e:
|
|
22
|
+
# uses: Steppy-dev/steppy/.github/workflows/steppy-e2e.yml@v0.3.0
|
|
23
|
+
# with:
|
|
24
|
+
# compose-file: docker-compose.yml
|
|
25
|
+
# secrets:
|
|
26
|
+
# steppy-env: ${{ secrets.STEPPY_ENV }}
|
|
27
|
+
#
|
|
28
|
+
# That form exposes verdict / exit-code / badge-percent as job outputs, but it IS
|
|
29
|
+
# the job — you cannot interleave your own steps into it. Keep this file if you
|
|
30
|
+
# need to (building an image before the run, for instance).
|
|
31
|
+
name: Steppy E2E
|
|
32
|
+
|
|
33
|
+
on:
|
|
34
|
+
push:
|
|
35
|
+
pull_request:
|
|
36
|
+
|
|
37
|
+
jobs:
|
|
38
|
+
e2e:
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v4
|
|
42
|
+
|
|
43
|
+
- uses: actions/setup-python@v5
|
|
44
|
+
with:
|
|
45
|
+
python-version: '3.12'
|
|
46
|
+
|
|
47
|
+
- name: Install Steppy
|
|
48
|
+
# Steppy is not on PyPI yet, so CI installs from git. Pin a tag rather
|
|
49
|
+
# than a branch: an e2e suite that silently changes its own tooling
|
|
50
|
+
# mid-week turns every unexplained failure into a two-hour bisect.
|
|
51
|
+
#
|
|
52
|
+
# The extras are what make `report.html` and `report.retries` /
|
|
53
|
+
# `steppy run --retries N` actually do something — without them Steppy
|
|
54
|
+
# warns and skips those features rather than failing the run.
|
|
55
|
+
run: pip install 'steppy-dev[retry,report] @ git+https://github.com/Steppy-dev/steppy.git@v0.3.0'
|
|
56
|
+
|
|
57
|
+
- name: Install Playwright browsers
|
|
58
|
+
run: playwright install chromium --with-deps
|
|
59
|
+
|
|
60
|
+
# In-app tours (only with `tours.enabled: true` in config.yaml): fail fast
|
|
61
|
+
# when the tour files committed to the app no longer match the scenarios.
|
|
62
|
+
# No browser, no app. It compares content for the build id each committed
|
|
63
|
+
# copy records, so a new commit alone never fails it. The app must then be
|
|
64
|
+
# BUILT with those files (`docker compose up --build` below) before the
|
|
65
|
+
# tour replays run against it.
|
|
66
|
+
# - name: Check the in-app tour files are current
|
|
67
|
+
# run: steppy tours --check
|
|
68
|
+
|
|
69
|
+
# Two things decide whether this step actually leaves you with a running
|
|
70
|
+
# app, and both are properties of YOUR compose file, not of this workflow:
|
|
71
|
+
#
|
|
72
|
+
# 1. `--wait` only waits on services that declare a `healthcheck:`. With
|
|
73
|
+
# no healthchecks it returns as soon as the containers are created,
|
|
74
|
+
# the scenarios start against a port that is not serving yet, and the
|
|
75
|
+
# run dies wholesale. This is the most common first-run failure.
|
|
76
|
+
# 2. The scenarios and your seed scripts run HERE, on the runner — not
|
|
77
|
+
# inside a container. So the app (and the database, if seeds touch it)
|
|
78
|
+
# must publish its ports, and apps.<role>.url points at localhost.
|
|
79
|
+
- name: Start the application stack
|
|
80
|
+
run: docker compose up -d --wait --wait-timeout 180
|
|
81
|
+
|
|
82
|
+
- name: Generate tests
|
|
83
|
+
run: steppy generate
|
|
84
|
+
|
|
85
|
+
- name: Run scenarios
|
|
86
|
+
env:
|
|
87
|
+
# Map your repository secrets onto the ${VAR} refs in config.yaml.
|
|
88
|
+
STEPPY_ADMIN_USERNAME: ${{ secrets.STEPPY_ADMIN_USERNAME }}
|
|
89
|
+
STEPPY_ADMIN_PASSWORD: ${{ secrets.STEPPY_ADMIN_PASSWORD }}
|
|
90
|
+
# --bundle records per-test and per-step events plus JUnit XML, which is
|
|
91
|
+
# what lets the report below name the step a failure died in instead of
|
|
92
|
+
# only the test. It changes the pytest invocation, so it is opt-in.
|
|
93
|
+
run: steppy run --trace --bundle
|
|
94
|
+
|
|
95
|
+
# When the suite goes red, the question is almost always "was it the tests
|
|
96
|
+
# or the stack?" — and the stack's own logs are the fastest way to know.
|
|
97
|
+
- name: Application logs
|
|
98
|
+
if: failure()
|
|
99
|
+
run: docker compose logs --no-color --tail 200
|
|
100
|
+
|
|
101
|
+
# A browsable report of this run: outcomes, failure causes grouped so one
|
|
102
|
+
# broken selector across forty scenarios reads as one row, the failing step
|
|
103
|
+
# named, and its screenshot. Self-contained and relative — open index.html
|
|
104
|
+
# straight out of the downloaded artifact.
|
|
105
|
+
#
|
|
106
|
+
# `if: always()` because a failing run is exactly when you want it, and
|
|
107
|
+
# `steppy report` exits 0 either way: it describes rendering, not the run.
|
|
108
|
+
- name: Build the run report
|
|
109
|
+
if: always()
|
|
110
|
+
run: steppy report
|
|
111
|
+
|
|
112
|
+
- name: Upload the report
|
|
113
|
+
if: always()
|
|
114
|
+
uses: actions/upload-artifact@v4
|
|
115
|
+
with:
|
|
116
|
+
name: steppy-report
|
|
117
|
+
path: output/report/
|
|
118
|
+
|
|
119
|
+
- name: Upload results
|
|
120
|
+
if: always()
|
|
121
|
+
uses: actions/upload-artifact@v4
|
|
122
|
+
with:
|
|
123
|
+
name: steppy-output
|
|
124
|
+
path: output/
|
|
125
|
+
|
|
126
|
+
# `-v` so the next run starts from a clean database rather than inheriting
|
|
127
|
+
# whatever the last one's seeds left behind.
|
|
128
|
+
- name: Stop the application stack
|
|
129
|
+
if: always()
|
|
130
|
+
run: docker compose down -v
|
|
131
|
+
|
|
132
|
+
# ── Publishing the report to GitHub Pages ─────────────────────────────────────
|
|
133
|
+
#
|
|
134
|
+
# ⚠ READ THIS FIRST. GitHub Pages is PUBLIC — on a public repo there is no access
|
|
135
|
+
# control, and a published screenshot is permanent and indexable. Your captures
|
|
136
|
+
# show your app with real-looking data in it. `screenshot.mask` (config.yaml) can
|
|
137
|
+
# redact fields at capture time, but a leak cannot be taken back.
|
|
138
|
+
#
|
|
139
|
+
# * This is for SHOWCASE and open-source repos, where the stack under test is
|
|
140
|
+
# seeded with throwaway data. For client work, keep the artifact upload above
|
|
141
|
+
# and read the report from there.
|
|
142
|
+
# * The report marks every page `noindex` and ships a `robots.txt` that
|
|
143
|
+
# disallows everything, so it will not be indexed by a well-behaved crawler.
|
|
144
|
+
# That is a courtesy, not a control.
|
|
145
|
+
# * `--screenshots failed` (the default) copies only the captures of variants
|
|
146
|
+
# that did not pass. Use `--screenshots all` deliberately, knowing it puts
|
|
147
|
+
# every step of every locale and layout on the public internet — and that on
|
|
148
|
+
# a large suite it is also what pushes the site towards Pages' size limit.
|
|
149
|
+
#
|
|
150
|
+
# One-time setup: repository Settings → Pages → Source: "GitHub Actions".
|
|
151
|
+
#
|
|
152
|
+
# Publish as an ARTIFACT, never by committing to a gh-pages branch: screenshots
|
|
153
|
+
# pushed on every run grow the repository's history forever and cannot be pruned
|
|
154
|
+
# without a rewrite.
|
|
155
|
+
#
|
|
156
|
+
# permissions:
|
|
157
|
+
# contents: read
|
|
158
|
+
# pages: write
|
|
159
|
+
# id-token: write
|
|
160
|
+
#
|
|
161
|
+
# jobs:
|
|
162
|
+
# # ... the e2e job above ...
|
|
163
|
+
#
|
|
164
|
+
# publish-report:
|
|
165
|
+
# # Default branch only. A report from every PR would publish work in
|
|
166
|
+
# # progress, and PR runs from forks cannot get an id-token anyway.
|
|
167
|
+
# if: always() && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
|
|
168
|
+
# needs: e2e
|
|
169
|
+
# runs-on: ubuntu-latest
|
|
170
|
+
# environment:
|
|
171
|
+
# name: github-pages
|
|
172
|
+
# url: ${{ steps.deploy.outputs.page_url }}
|
|
173
|
+
# steps:
|
|
174
|
+
# - uses: actions/download-artifact@v4
|
|
175
|
+
# with:
|
|
176
|
+
# name: steppy-report
|
|
177
|
+
# path: site
|
|
178
|
+
# - uses: actions/configure-pages@v5
|
|
179
|
+
# - uses: actions/upload-pages-artifact@v3
|
|
180
|
+
# with:
|
|
181
|
+
# path: site
|
|
182
|
+
# - id: deploy
|
|
183
|
+
# uses: actions/deploy-pages@v4
|
|
184
|
+
#
|
|
185
|
+
# A repository has exactly ONE Pages site. If you also publish generated guides
|
|
186
|
+
# (`steppy build`), decide the tree before anything links to it — e.g. the report
|
|
187
|
+
# under site/report/ and the guides under site/guides/ — because moving the
|
|
188
|
+
# report later also moves badge.json, and that URL is a contract with every
|
|
189
|
+
# README that points at it.
|
|
190
|
+
#
|
|
191
|
+
# ── The e2e-surface coverage badge ────────────────────────────────────────────
|
|
192
|
+
#
|
|
193
|
+
# `steppy report` writes a shields endpoint at badge.json in the site root, so
|
|
194
|
+
# once the Pages job above is live the badge is served at <page_url>/badge.json:
|
|
195
|
+
#
|
|
196
|
+
# 
|
|
197
|
+
#
|
|
198
|
+
# Three prerequisites, in order — miss any one and there is no number:
|
|
199
|
+
#
|
|
200
|
+
# 1. badge.json is written ONLY when the run recorded coverage. Without
|
|
201
|
+
# `steppy run --coverage` the report has no coverage section and no badge
|
|
202
|
+
# file at all, and the shields URL 404s.
|
|
203
|
+
# 2. A percentage needs a DECLARED surface to score against, which means a
|
|
204
|
+
# `coverage:` block in config.yaml naming `coverage.apis[].openapi` (a URL
|
|
205
|
+
# the runner can reach — your compose stack serving /openapi.json qualifies)
|
|
206
|
+
# and optionally `coverage.sitemap`. With coverage recorded but nothing
|
|
207
|
+
# declared, the badge honestly reads `n/a` in grey rather than 0%.
|
|
208
|
+
# 3. `steppy coverage refresh` EXITS 1 when no `coverage:` block is configured,
|
|
209
|
+
# which is why these steps are not armed by default. Add them once you have
|
|
210
|
+
# declared a surface.
|
|
211
|
+
#
|
|
212
|
+
# Two things worth knowing before you turn it on:
|
|
213
|
+
#
|
|
214
|
+
# * Recording drives CDP precise coverage, which costs runtime. On free
|
|
215
|
+
# runners, recording it on default-branch pushes only — and not on every PR
|
|
216
|
+
# — is usually the right trade.
|
|
217
|
+
# * Declare `coverage.non_ui` in config.yaml (health checks, webhook receivers,
|
|
218
|
+
# OAuth callbacks, internal cron). Triage flags set with `steppy coverage
|
|
219
|
+
# triage` live in the gitignored .steppy/coverage.db, so a fresh CI checkout
|
|
220
|
+
# has none and every one of those endpoints counts against you.
|
|
221
|
+
#
|
|
222
|
+
# - name: Run scenarios
|
|
223
|
+
# run: steppy run --trace --bundle --coverage
|
|
224
|
+
#
|
|
225
|
+
# - name: Refresh the declared surface
|
|
226
|
+
# if: always()
|
|
227
|
+
# run: steppy coverage refresh && steppy coverage report --json output/coverage/gaps.json
|
|
228
|
+
#
|
|
229
|
+
# ── Results bundles ───────────────────────────────────────────────────────────
|
|
230
|
+
#
|
|
231
|
+
# Instead of uploading a loose output/ tree, package the run into one portable,
|
|
232
|
+
# self-describing archive. The run step already passes --bundle, so the event
|
|
233
|
+
# records and JUnit XML a bundle needs are captured; add the packaging step:
|
|
234
|
+
#
|
|
235
|
+
# - name: Package results
|
|
236
|
+
# if: always()
|
|
237
|
+
# run: steppy bundle create
|
|
238
|
+
#
|
|
239
|
+
# - name: Upload results
|
|
240
|
+
# if: always()
|
|
241
|
+
# uses: actions/upload-artifact@v4
|
|
242
|
+
# with:
|
|
243
|
+
# name: steppy-bundle
|
|
244
|
+
# path: output/bundles/
|
|
245
|
+
#
|
|
246
|
+
# The archive holds output/ exactly as steppy wrote it, so `tar -xf` then
|
|
247
|
+
# `steppy report` / `steppy guide` / `steppy build` works unchanged on a
|
|
248
|
+
# downloaded artifact. Adding the `bundle` extra to the install line above gets
|
|
249
|
+
# zstd compression; without it bundles are .tar.gz.
|
|
250
|
+
|
|
251
|
+
# ── Running in parallel ───────────────────────────────────────────────────────
|
|
252
|
+
#
|
|
253
|
+
# For a large suite, swap the run step for `steppy run --workers 4 --trace`. Note
|
|
254
|
+
# what that changes about the compose steps above: the coordinator brings up ONE
|
|
255
|
+
# FULL STACK PER WORKER and tears them down itself, so you drop the explicit
|
|
256
|
+
# `docker compose up` and give it `parallel.compose_file` instead. Four copies of
|
|
257
|
+
# your app on one hosted runner is a lot — this earns its keep on a large suite
|
|
258
|
+
# or a beefy self-hosted runner, not on a thirty-scenario suite.
|
|
259
|
+
#
|
|
260
|
+
# It needs a `parallel:` block + per-app `internal_url` in config.yaml,
|
|
261
|
+
# healthchecks on your compose services, and Docker Compose 2.20+ on the runner
|
|
262
|
+
# (ubuntu-latest has it). Two things worth knowing about the CI output:
|
|
263
|
+
#
|
|
264
|
+
# * Failed scenarios are automatically re-run once at reduced concurrency. One
|
|
265
|
+
# that then passes is reported as FLAKY and does NOT fail the build — it lost
|
|
266
|
+
# a race, it is not broken. Pass --strict-flaky if you would rather CI go red
|
|
267
|
+
# on flakes, or --retry-pass 0 to turn the retry off entirely.
|
|
268
|
+
# * If the app or auth is down, the run aborts after ~10 scenarios instead of
|
|
269
|
+
# burning the full wall clock on 300 identical failures.
|
|
270
|
+
#
|
|
271
|
+
# `steppy flaky` ranks repeat offenders across runs, which is only useful if the
|
|
272
|
+
# history survives between builds. Cache it around the run step:
|
|
273
|
+
#
|
|
274
|
+
# - uses: actions/cache@v4
|
|
275
|
+
# with:
|
|
276
|
+
# path: .steppy/history.jsonl
|
|
277
|
+
# key: steppy-history-${{ github.run_id }}
|
|
278
|
+
# restore-keys: steppy-history-
|
|
279
|
+
#
|
|
280
|
+
# ...then add a reporting step after the run:
|
|
281
|
+
#
|
|
282
|
+
# - name: Flakiest scenarios
|
|
283
|
+
# if: always()
|
|
284
|
+
# run: steppy flaky --last 20 --min-rate 0.1
|