Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/handbook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -317,13 +317,13 @@ jobs:
- name: Stage web e2e baselines from web repo
env:
WEB_DIR: _web-checkout
# RealUnitCH/web commits 18 Playwright visual baselines under
# RealUnitCH/web commits 22 Playwright visual baselines under
# tests/__screenshots__/{desktop-chromium,tablet-chromium,mobile-safari}/.
# `-ne` below fails the build on ANY drift (add OR remove) so a change
# to the committed set upstream requires an explicit bump here AND a
# matching card edit in docs/handbook/de/index.html (#spec-web) in the
# SAME change — same guard rationale as the api steps above.
EXPECTED_WEB_BASELINE_COUNT: 18
EXPECTED_WEB_BASELINE_COUNT: 22
run: |
set -euo pipefail

Expand Down
52 changes: 24 additions & 28 deletions .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,25 @@ name: RealUnit Build
# below (saves macOS-runner minutes while work is still in progress).
# - `labeled` / `unlabeled` are intentionally omitted: realunit-app has
# no label-gated heavy job here (Tier 3 has its own workflow).
# - `push: develop` and `push: staging` keep post-merge verification
# as an authoritative source of truth, independent of any PR state.
# `staging` participates because feature → staging merges land an
# integration commit whose verification cannot be deferred to the
# auto-opened staging → develop PR (the gate must be authoritative
# at the moment the commit lands on staging). Other branches do not
# need a post-push check — the PR-event already covers them.
# - `push: develop` keeps post-merge verification as an authoritative
# source of truth. Staging fallback routing lives in the separate
# `staging-ci-fallback.yaml` workflow: it dispatches this workflow
# only when no ready staging → develop PR owns CI for the pushed SHA.
# - `workflow_dispatch` is kept as a manual override and is NOT
# draft-gated (see the job `if:` guard below).
on:
workflow_dispatch:
push:
branches: [develop, staging]
branches: [develop]
pull_request:
branches-ignore: [main]
types: [opened, synchronize, reopened, ready_for_review]

# Group by PR number so a new push to the same PR cancels the
# in-flight run for the outdated commit. macOS runners are scarce —
# letting an obsolete run finish wastes ~10 minutes per push. Grouping
# by SHA (the previous approach) put every commit in its own group,
# which meant no cancellation ever happened and back-to-back pushes
# queued sequentially.
# Falls back to `github.ref` for `push` and `workflow_dispatch` events
# (where there is no `pull_request.number`): `refs/heads/develop` for
# post-merge runs, the dispatched ref for manual triggers.
# Group PR runs by PR number, and push/dispatch runs by ref. A new SHA
# cancels work for the outdated SHA in the same lane. Staging push
# routing is isolated in `staging-ci-fallback.yaml`; it either relies on
# this PR workflow or dispatches this workflow as a fallback, but never
# shares a concurrency group with the PR run.
concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
Expand All @@ -60,7 +53,7 @@ jobs:
runs-on: macos-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.41.6"
Expand Down Expand Up @@ -151,7 +144,7 @@ jobs:

- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: coverage-lcov
path: coverage/lcov.info
Expand All @@ -168,7 +161,7 @@ jobs:
# fail-open (silent `exit 0`).
- name: Upload coverage summary
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: coverage-summary
path: coverage/lcov.summary
Expand Down Expand Up @@ -228,16 +221,16 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Download coverage summary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: coverage-summary
path: coverage

- name: Download scoped coverage tracefile
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: coverage-lcov
path: coverage
Expand Down Expand Up @@ -324,7 +317,7 @@ jobs:
# the slot back twice as fast as 30.
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.41.6"
Expand All @@ -344,7 +337,7 @@ jobs:
run: flutter test test/goldens
- name: Upload golden diff on failure
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: golden-diffs
path: test/goldens/**/failures/**
Expand All @@ -361,10 +354,13 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: "1.25.5"
# This repository has no Go module; caching would only emit a
# misleading "go.sum not found" warning for this audit-only job.
cache: false

- name: Install bitbox-audit
continue-on-error: true
Expand Down Expand Up @@ -392,7 +388,7 @@ jobs:

- name: Upload audit report
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: bitbox-audit-report
path: bitbox-audit-report.md
Expand Down
77 changes: 77 additions & 0 deletions .github/workflows/staging-ci-fallback.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Staging CI Router

# A staging push normally produces a `synchronize` event on the open
# staging → develop promotion PR. That PR run owns the canonical required
# checks. If the PR is missing, draft, stale, or cannot be queried, this
# workflow dispatches the canonical RealUnit Build as a fail-safe.
on:
push:
branches: [staging]
workflow_dispatch:

permissions:
actions: write
contents: read
pull-requests: read

# Only the newest staging SHA needs routing. This group is deliberately
# isolated from RealUnit Build, so it can never cancel required PR checks.
concurrency:
group: staging-ci-router-${{ github.ref }}
cancel-in-progress: true

jobs:
route:
name: Route staging CI
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Use PR CI or dispatch fail-safe CI
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

SHOULD_DISPATCH=true
REASON="manual routing request"

if [ "${GITHUB_EVENT_NAME}" = "push" ]; then
REASON="promotion PR lookup failed"
if PULLS_JSON="$(
gh api --method GET "/repos/${GITHUB_REPOSITORY}/pulls" \
-f state=open \
-f base=develop \
-f head="${GITHUB_REPOSITORY_OWNER}:staging" \
-f per_page=100
)"; then
if jq -e \
--arg repo "${GITHUB_REPOSITORY}" \
--arg sha "${GITHUB_SHA}" \
'any(.[]; .draft == false and .head.repo.full_name == $repo and .head.sha == $sha)' \
<<<"${PULLS_JSON}" >/dev/null; then
SHOULD_DISPATCH=false
REASON="ready promotion PR at this SHA owns canonical CI"
else
REASON="no ready promotion PR at this SHA"
fi
else
echo "::warning::Could not query the staging promotion PR; dispatching fail-safe CI"
fi
fi

if [ "${SHOULD_DISPATCH}" = "true" ]; then
gh workflow run pull-request.yaml \
--repo "${GITHUB_REPOSITORY}" \
--ref staging
echo "::notice::Dispatched RealUnit Build on staging (${REASON})"
else
echo "::notice::No fallback dispatch needed (${REASON})"
fi

{
echo "### Staging CI routing"
echo
echo "- Fallback dispatched: \`${SHOULD_DISPATCH}\`"
echo "- Reason: ${REASON}"
echo "- SHA: \`${GITHUB_SHA}\`"
} >> "${GITHUB_STEP_SUMMARY}"
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ Tier 1 specs live under `test/integration/**` and run inside the same `flutter t

| Workflow | Trigger | Action |
| ---------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `pull-request.yaml` | Any PR except PRs to `main` · push `develop` · manual | `flutter analyze` + `flutter test --coverage --exclude-tags golden`, scope lcov to the activated surface, fail below the committed floor, upload lcov artifact. In parallel, the `Visual Regression` job runs `flutter test test/goldens` on the self-hosted runner against the committed pixel baselines under `test/goldens/**/goldens/macos/` and uploads diff PNGs on failure. Jobs: `Analyze & Test`, `Coverage Floor Gate`, `Visual Regression`, `BitBox quirks audit`. |
| `pull-request.yaml` | Any PR except PRs to `main` · push `develop` · manual | Runs `flutter analyze` + `flutter test --coverage --exclude-tags golden`, enforces scoped coverage, validates Goldens on the self-hosted runner, and uploads diagnostics. Jobs: `Analyze & Test`, `Coverage Floor Gate`, `Visual Regression`, `BitBox quirks audit`. |
| `staging-ci-fallback.yaml` | Push `staging` · manual | Routes staging CI without cross-event cancellation: a ready `staging → develop` PR owns canonical CI; otherwise it dispatches `pull-request.yaml` on `staging` as a fail-safe. |
| `tier3-handbook.yaml` | Any PR except PRs to `main`, with label `tier3:full` · push `develop` · manual | Tier-3 navigation/tap-routing smoke: runs every `.maestro/handbook/*.yaml` flow on a fresh `iPhone 17` simulator and uploads diagnostic captures (`build/handbook-captures/`) as a build artifact. Pixel drift on the page renders is owned by `Visual Regression` in `pull-request.yaml`, not this job. |
| `bitbox-simulator.yml` | Any PR except PRs to `main` touching `lib/packages/hardware_wallet/**`, `lib/packages/wallet/**`, `lib/screens/hardware_connect_bitbox/**`, their test mirrors, `pubspec.yaml`, or the workflow itself · manual | Runs the BitBox02 firmware simulator with `bitbox-testkit` baselines (Tier 2) |
| `bitbox-simulator-slash.yml` | `/bitbox-simulator` comment on any PR | Same engine as above, on-demand per PR (variants: default / `ref=main`) |
Expand Down
70 changes: 70 additions & 0 deletions docs/handbook/de/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8183,6 +8183,76 @@ <h3>Adressbestätigung — ungültiger Link <a class="src" href="https://github.
</div>
</div>

<h3>Adressbestätigung — keine Wallet-Registrierung <a class="src" href="https://github.com/RealUnitCH/web/tree/develop/tests/__screenshots__" title="Quelle: tests/__screenshots__/">↗</a></h3>
<div class="tests cols-2">
<div class="test" id="web-confirm-no-registration-desktop-de">
<div class="head">
<a class="name permalink" href="#web-confirm-no-registration-desktop-de">web-confirm-no-registration-desktop-de</a>
<button class="copy-link" type="button" data-target="web-confirm-no-registration-desktop-de" title="Direkt-Link kopieren" aria-label="Direkt-Link kopieren">🔗 Link</button>
<span class="src">desktop-chromium/confirm-no-registration.png</span>
</div>
<div class="img">
<img
src="../web/desktop-chromium/confirm-no-registration.png"
alt="Keine Wallet-Registrierung — Desktop (DE)"
/>
</div>
<div class="desc">
Zustand, wenn die E-Mail-Adresse bestätigt ist, aber keine
Wallet-Registrierung zum Link gefunden wurde — permanenter
Zustand ohne Retry, Desktop, Deutsch.
</div>
</div>
<div class="test" id="web-confirm-no-registration-desktop-en">
<div class="head">
<a class="name permalink" href="#web-confirm-no-registration-desktop-en">web-confirm-no-registration-desktop-en</a>
<button class="copy-link" type="button" data-target="web-confirm-no-registration-desktop-en" title="Direkt-Link kopieren" aria-label="Direkt-Link kopieren">🔗 Link</button>
<span class="src">desktop-chromium/confirm-no-registration-en.png</span>
</div>
<div class="img">
<img
src="../web/desktop-chromium/confirm-no-registration-en.png"
alt="No wallet registration found — Desktop (EN)"
/>
</div>
<div class="desc">
Derselbe Zustand auf Englisch.
</div>
</div>
<div class="test" id="web-confirm-no-registration-tablet">
<div class="head">
<a class="name permalink" href="#web-confirm-no-registration-tablet">web-confirm-no-registration-tablet</a>
<button class="copy-link" type="button" data-target="web-confirm-no-registration-tablet" title="Direkt-Link kopieren" aria-label="Direkt-Link kopieren">🔗 Link</button>
<span class="src">tablet-chromium/confirm-no-registration.png</span>
</div>
<div class="img">
<img
src="../web/tablet-chromium/confirm-no-registration.png"
alt="Keine Wallet-Registrierung — Tablet"
/>
</div>
<div class="desc">
Zustand im Tablet-Layout.
</div>
</div>
<div class="test" id="web-confirm-no-registration-mobile">
<div class="head">
<a class="name permalink" href="#web-confirm-no-registration-mobile">web-confirm-no-registration-mobile</a>
<button class="copy-link" type="button" data-target="web-confirm-no-registration-mobile" title="Direkt-Link kopieren" aria-label="Direkt-Link kopieren">🔗 Link</button>
<span class="src">mobile-safari/confirm-no-registration.png</span>
</div>
<div class="img">
<img
src="../web/mobile-safari/confirm-no-registration.png"
alt="Keine Wallet-Registrierung — Mobile"
/>
</div>
<div class="desc">
Zustand in der Mobile-Ansicht.
</div>
</div>
</div>

<h3>Adressbestätigung — Dienst nicht verfügbar <a class="src" href="https://github.com/RealUnitCH/web/tree/develop/tests/__screenshots__" title="Quelle: tests/__screenshots__/">↗</a></h3>
<div class="tests cols-2">
<div class="test" id="web-confirm-unavailable-desktop-de">
Expand Down
Loading
Loading