From d0c00a761bc131495225a9b0814918be53c1fd29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Kr=C3=BCger?= Date: Fri, 12 Jun 2026 16:25:40 +0200 Subject: [PATCH 1/3] =?UTF-8?q?ci:=20mechanical=20quality=20gates=20?= =?UTF-8?q?=E2=80=94=20close=20the=20golden-bot=20hole,=20CODEOWNERS,=20ra?= =?UTF-8?q?tchet=20guard,=20supply-chain=20monitoring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every rule below existed as documentation or convention; this PR makes each one a machine gate (or an automatic process), because a guideline without enforcement silently rots: - golden-regenerate: push baselines via TAG_DEPLOY_KEY instead of GITHUB_TOKEN. Token pushes do not trigger workflows, so the bot-pushed head SHA had ZERO checks while GitHub showed the PR as mergeable — required checks never ran against regenerated baselines unless a human remembered the empty-commit re-arm (documented gotcha in docs/visual-regression-tests.md). Deploy-key pushes trigger the full PR gate automatically. - CODEOWNERS: signing/hardware-wallet paths, pubspec, the floor files and .github/ itself now route to maintainers. Today one approval from anyone with write access suffices, even for ceremony code. - floor-ratchet-guard: lowering .coverage-floor-{lines,functions} without the coverage:lower-floor label now fails CI. The label was a documented convention no workflow checked — a quiet floor edit inside a big diff could de-arm the Coverage Floor Gate permanently. - pr-policy: conventional PR titles + 1500-line size cap (size:override label for goldens/codegen churn). - dependabot (pub + github-actions, 7-day cooldown) and dependency-review on PR diffs: the repo currently has zero automated dependency monitoring; the cooldown window would have blocked every major 2025 supply-chain incident. - PR template encoding the existing testing/handbook/API-authority rules as a checklist. --- .github/CODEOWNERS | 24 +++++++ .github/PULL_REQUEST_TEMPLATE.md | 26 ++++++++ .github/dependabot.yml | 35 ++++++++++ .github/workflows/dependency-review.yml | 27 ++++++++ .github/workflows/golden-regenerate.yaml | 26 ++++++-- .github/workflows/pull-request.yaml | 82 ++++++++++++++++++++++++ 6 files changed, 213 insertions(+), 7 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/dependency-review.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..e8786b0f0 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,24 @@ +# Code owners for security-critical paths. +# +# Why this file exists: the repo's rulesets require 1 approval from anyone +# with write access — for a wallet app that is too thin on the paths that +# touch signing ceremonies, key material and the CI gates themselves. With +# "Require review from Code Owners" enabled in the develop/staging/main +# rulesets, the people below become a mandatory second pair of eyes exactly +# where a mistake costs user funds. Until an admin flips that ruleset +# setting, this file still auto-requests their review on matching PRs. + +# Signing ceremonies & key handling +/lib/packages/wallet/ @davidleomay @TaprootFreak @konstantinullrich +/lib/packages/hardware_wallet/ @davidleomay @TaprootFreak @konstantinullrich +/lib/screens/hardware_connect_bitbox/ @davidleomay @TaprootFreak @konstantinullrich + +# Dependency manifests (supply chain) +/pubspec.yaml @davidleomay @TaprootFreak @konstantinullrich +/pubspec.lock @davidleomay @TaprootFreak @konstantinullrich + +# CI and the gates themselves — a gate nobody guards is not a gate +/.github/ @davidleomay @TaprootFreak @konstantinullrich +/.coverage-floor-lines @davidleomay @TaprootFreak @konstantinullrich +/.coverage-floor-functions @davidleomay @TaprootFreak @konstantinullrich +/tool/ @davidleomay @TaprootFreak @konstantinullrich diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..ef720e6c6 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,26 @@ +## What + + + +## Tiers (docs/testing.md) + +- [ ] Tier 0/1 (unit/widget) cover the change — coverage floor holds (CI enforces it) +- [ ] Touches `hardware_wallet`/`wallet` paths → Tier 2 (BitBox simulator) runs on this PR +- [ ] UI flow changed → `tier3:full` label set, or post-merge develop run is sufficient because: +- [ ] Platform-coupled code without integration test carries `// @no-integration-test: ` + +## Goldens & handbook + +- [ ] No visual change — baselines untouched +- [ ] Baselines regenerated via `golden-regenerate.yaml` (CI re-runs automatically on the bot commit) + +## API authority (CONTRIBUTING.md) + +- [ ] No new client-side decision logic — server capability flags consumed as-is +- [ ] Pair-PR with the API repo linked here if a capability is consumed: + +## Security checklist + +- [ ] Touches signing, key handling, or `pubspec` → code-owner review requested +- [ ] No secret, seed, or PII logged or persisted outside the secure storage paths +- [ ] New dependency added → justified here: diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..cb9c97e87 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,35 @@ +# Dependency-update automation. +# +# Why: this repo currently has no automated dependency monitoring at all — +# a known-vulnerable transitive package would sit unnoticed until a human +# happens to read an advisory. For a wallet app the dependency tree is the +# single largest attack surface (the 2025 npm supply-chain worms made that +# concrete); pub is smaller but not immune, and GitHub Actions are code we +# execute with repo credentials. +# +# The 7-day cooldown is deliberate: freshly published versions are the +# riskiest (compromised-maintainer releases get yanked within days). Waiting +# a week would have blocked every major 2025 supply-chain incident while +# costing nothing for legitimate updates. + +version: 2 +updates: + - package-ecosystem: pub + directory: / + schedule: + interval: weekly + day: monday + target-branch: staging + cooldown: + default-days: 7 + open-pull-requests-limit: 5 + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + target-branch: staging + cooldown: + default-days: 7 + open-pull-requests-limit: 5 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 000000000..93228fa67 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,27 @@ +name: Dependency Review + +# Blocks PRs that introduce dependencies with known vulnerabilities. +# +# Why: Dependabot (see .github/dependabot.yml) watches the EXISTING tree on a +# schedule; this action gates the PR DIFF itself — a newly added pub package +# or GitHub Action with a known high-severity advisory cannot enter the repo +# in the first place. GitHub's dependency graph covers pub, so Dart +# dependencies are in scope. Zero maintenance, first-party action. + +on: + pull_request: + branches-ignore: [main] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + fail-on-severity: high + comment-summary-in-pr: on-failure diff --git a/.github/workflows/golden-regenerate.yaml b/.github/workflows/golden-regenerate.yaml index a958790d7..111e29a2a 100644 --- a/.github/workflows/golden-regenerate.yaml +++ b/.github/workflows/golden-regenerate.yaml @@ -28,10 +28,11 @@ concurrency: group: golden-regenerate-${{ github.ref }} cancel-in-progress: true -# `contents: write` is load-bearing: the auto-commit step pushes back to -# the dispatched branch. The default `GITHUB_TOKEN` permission is `read`. +# Push happens via the TAG_DEPLOY_KEY ssh-agent (see below), not the +# GITHUB_TOKEN — token pushes would leave the new head SHA without any +# triggered checks. `contents: read` is enough for checkout. permissions: - contents: write + contents: read jobs: regenerate: @@ -40,11 +41,22 @@ jobs: timeout-minutes: 30 steps: - uses: actions/checkout@v4 + + # Push via the existing deploy key (same key auto-tag.yaml uses) instead + # of GITHUB_TOKEN. This closes a documented gate hole: pushes made with + # GITHUB_TOKEN deliberately do NOT trigger workflows, so the bot-pushed + # baseline commit used to sit at the PR head with ZERO status checks — + # and GitHub shows such a PR as mergeable. Required checks never ran + # against the new baselines unless a human remembered the empty-commit + # re-arm. Deploy-key pushes DO trigger workflows, so the full PR gate + # re-runs on the regenerated head automatically. + - name: Setup SSH for Deploy Key + uses: webfactory/ssh-agent@v0.9.0 with: - # Explicit token so the later `git push` is authenticated. Without - # this, checkout still works but the remote is configured with no - # credential helper and the push fails with HTTP 403. - token: ${{ secrets.GITHUB_TOKEN }} + ssh-private-key: ${{ secrets.TAG_DEPLOY_KEY }} + + - name: Configure Git for SSH + run: git remote set-url origin git@github.com:${{ github.repository }}.git - uses: subosito/flutter-action@v2 with: flutter-version: "3.41.6" diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 43276344f..b47525620 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -364,3 +364,85 @@ jobs: name: bitbox-audit-report path: bitbox-audit-report.md if-no-files-found: warn + + # --------------------------------------------------------------------------- + # Coverage Floor Ratchet Guard + # + # docs/testing.md defines the ratchet protocol: lowering a floor file needs + # reviewer sign-off, made visible via the `coverage:lower-floor` label. Until + # now that was convention only — a quiet one-line edit to + # `.coverage-floor-lines` inside a big diff could slip through review and + # silently de-arm the Coverage Floor Gate for every future PR. This job makes + # the protocol mechanical: a floor decrease without the label fails the PR. + # Raising a floor (the normal ratchet direction) passes without ceremony. + floor-ratchet-guard: + name: Coverage Floor Ratchet Guard + if: github.event_name == 'pull_request' && github.event.pull_request.draft == false + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Floor may only decrease with the coverage:lower-floor label + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HAS_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'coverage:lower-floor') }} + run: | + set -euo pipefail + rc=0 + for f in .coverage-floor-lines .coverage-floor-functions; do + BASE="$(git show "$BASE_SHA:$f" 2>/dev/null || echo 0)" + HEAD="$(cat "$f")" + echo "$f: base=$BASE head=$HEAD" + if awk -v a="$HEAD" -v b="$BASE" 'BEGIN { exit !(a < b) }'; then + if [ "$HAS_LABEL" != "true" ]; then + echo "::error::$f was lowered ($BASE -> $HEAD) without the 'coverage:lower-floor' label. Lowering a floor requires explicit reviewer sign-off via the label (docs/testing.md ratchet protocol)." + rc=1 + else + echo "$f lowered with label — allowed." + fi + fi + done + exit "$rc" + + # --------------------------------------------------------------------------- + # PR Policy + # + # Two cheap review-quality gates: + # - Conventional-commit PR titles keep `git log` greppable and the release + # notes readable (the repo already uses the convention; this just stops + # drift). + # - The size cap exists because review quality degrades sharply with diff + # size — and for a wallet app, an under-reviewed PR is a security risk, + # not a style problem. Generated code (goldens, codegen, pubspec.lock) + # legitimately exceeds the cap: justify it and add the `size:override` + # label. + pr-policy: + name: PR Policy + if: github.event_name == 'pull_request' && github.event.pull_request.draft == false + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Conventional PR title + env: + TITLE: ${{ github.event.pull_request.title }} + run: | + set -euo pipefail + if ! echo "$TITLE" | grep -qE '^(feat|fix|docs|chore|refactor|test|ci|build|perf|style|revert)(\([a-z0-9._/ -]+\))?!?: .+'; then + echo "::error::PR title '$TITLE' does not follow the conventional-commit format, e.g. 'fix(wallet): reject stale BitBox pairing state'" + exit 1 + fi + - name: Size cap (1500 changed lines, label size:override to bypass) + env: + ADDITIONS: ${{ github.event.pull_request.additions }} + DELETIONS: ${{ github.event.pull_request.deletions }} + HAS_OVERRIDE: ${{ contains(github.event.pull_request.labels.*.name, 'size:override') }} + run: | + set -euo pipefail + TOTAL=$((ADDITIONS + DELETIONS)) + echo "changed lines: $TOTAL" + if [ "$TOTAL" -gt 1500 ] && [ "$HAS_OVERRIDE" != "true" ]; then + echo "::error::PR changes $TOTAL lines (cap 1500). Split it, or add the 'size:override' label with a justification in the description (goldens/codegen/lockfile churn qualifies)." + exit 1 + fi From f95a75baa98939b279b056272b39b040a2998aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Kr=C3=BCger?= Date: Fri, 12 Jun 2026 16:28:15 +0200 Subject: [PATCH 2/3] ci: dependency-review non-blocking until admins enable the Dependency graph --- .github/workflows/dependency-review.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 93228fa67..e5e0b3136 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -19,6 +19,12 @@ jobs: dependency-review: runs-on: ubuntu-latest timeout-minutes: 5 + # TEMPORARY non-blocking: the action needs the repo's Dependency graph, + # which is currently disabled (admin setting: + # https://github.com/RealUnitCH/app/settings/security_analysis). + # Flip this to blocking (delete continue-on-error) in the same PR/commit + # that enables the graph — a permanently yellow gate is not a gate. + continue-on-error: true steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 From e8dda30e5f5d87137217703096c5aa1d5b2ee0a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Kr=C3=BCger?= Date: Fri, 12 Jun 2026 16:29:33 +0200 Subject: [PATCH 3/3] =?UTF-8?q?ci:=20dependency-review=20self-arms=20?= =?UTF-8?q?=E2=80=94=20probe=20Dependency=20graph,=20warn+skip=20while=20d?= =?UTF-8?q?isabled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dependency-review.yml | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index e5e0b3136..3edd2ebc2 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -19,15 +19,28 @@ jobs: dependency-review: runs-on: ubuntu-latest timeout-minutes: 5 - # TEMPORARY non-blocking: the action needs the repo's Dependency graph, - # which is currently disabled (admin setting: - # https://github.com/RealUnitCH/app/settings/security_analysis). - # Flip this to blocking (delete continue-on-error) in the same PR/commit - # that enables the graph — a permanently yellow gate is not a gate. - continue-on-error: true steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + # The action hard-fails when the repo's Dependency graph is disabled + # (admin setting). Probe first: with the graph off, emit a loud warning + # and skip — with it on, the gate arms itself automatically, no + # follow-up commit needed. + - name: Check Dependency graph availability + id: graph + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + if gh api "repos/${{ github.repository }}/dependency-graph/sbom" --silent >/dev/null 2>&1; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + else + echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "::warning::Dependency graph is disabled for this repository — dependency review SKIPPED. An admin should enable it: https://github.com/${{ github.repository }}/settings/security_analysis" + fi + - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + if: steps.graph.outputs.enabled == 'true' with: fail-on-severity: high comment-summary-in-pr: on-failure