Skip to content

ci(i18n): audit main pushes, not just PRs into it - #535

Merged
ryanbr merged 1 commit into
mainfrom
fix/i18n-audit-on-main-push
Jul 17, 2026
Merged

ryanbr merged 1 commit into
mainfrom
fix/i18n-audit-on-main-push

Conversation

@ryanbr

@ryanbr ryanbr commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Why

A release pushes straight to main with no PR, and Tools/appchangelog-gen.py emits the What's New title as a raw Kotlin literal that has to be localized by hand afterwards (Compose has no Xcode-style auto-extraction).

With a pull_request-only trigger, that literal landed completely unaudited — and because --ci is a whole-tree gate, not a diff, it then red-checked every open PR on a line none of them touched.

That's exactly what 9.0.1 did (fixed by #514): the break was invisible where it was introduced, and highly visible everywhere it wasn't. #507, #512 and #513 all went red on it. Nothing currently prevents the next release doing the same.

What

Audit pushes to main too, so a release-introduced literal surfaces on main the moment it lands and gets fixed before any contributor trips over it.

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

github.base_ref is set only for pull_request and is empty on a push (which would pass a bare origin/), so it falls back to main. The ref is retained for workflow compatibility — --ci is a whole-tree gate, so the resolved value doesn't change the verdict.

Same 8-second ubuntu job. No new cost, no behaviour change for PRs.

Verification

  • YAML parses; both triggers resolve (pull_request + push, both [main]).
  • The push path's exact command — python3 Tools/i18n_audit.py --ci origin/main — passes on current main (OK no hardcoded literals).

Notes

This makes the failure loud and immediate rather than preventing it. appchangelog-gen can't fully self-solve: it could emit the uiString(R.string.…) key + the en string, but it can't translate, so de/es/fr would be missing and the coverage half would still fail — the localize step stays manual, which is precisely why it needs a guard. Gating fork-release.yml on the audit would be the stronger (release-blocking) step; deliberately left as a separate call.

A release pushes STRAIGHT to main with no PR, and appchangelog-gen.py emits the
What's New title as a RAW Kotlin literal that has to be localized by hand
afterwards. With a PR-only trigger that landed completely unaudited — and
because --ci is a whole-tree gate rather than a diff, it then red-checked EVERY
open PR on a line none of them touched. That is exactly what 9.0.1 did (fixed
by #514): the break was invisible where it was introduced and highly visible
everywhere it wasn't.

Audit pushes to main so a release-introduced literal surfaces on main the
moment it lands, and gets fixed before any contributor trips over it. Same
8-second ubuntu job, no new cost.

github.base_ref is set only for pull_request and is empty on a push (which
would pass a bare 'origin/'), so fall back to main. The ref is retained for
workflow compatibility — --ci is a whole-tree gate, so the resolved value does
not change the verdict.

YAML parses (both triggers); the push path's exact command
'i18n_audit.py --ci origin/main' passes on current main.
@ryanbr
ryanbr merged commit d79cd29 into main Jul 17, 2026
1 check passed
ryanbr added a commit that referenced this pull request Jul 17, 2026
Numbers are main's current 292/203 +1, matching the previous staging commit's
pattern, so the .staging app upgrades cleanly over the last testing build (290)
and stays above main's released high-water.

Carries everything merged since the 9.0.1 release:
 - Workouts read HR under the recording strap, not a hardcoded id (#512 Kotlin,
   #513 Swift twin)
 - Oura 0x60/0x80 IBI decoders corrected to the ring's real byte layout (#511)
 - Whoop5Config enable_sig12 corrected to ASCII '1' (#522)
 - Sleep time edits save as one validated window (#525)
 - "Recompute this night" for deleted sleep windows (#526)
 - Stress marker values stay readable (#507)
 - Faster history sync + faster Bluetooth link, both EXPERIMENTAL and default-off
   (#536/#537/#538) - the whole point of this build is field data on those
 - i18n changelog-title fix + main-push audit (#514/#535)

versionName stays 9.0.2 here for the staging identity only; the real 9.0.2
release still bumps main separately (and must be raised past 293 first).
JNyyyyy pushed a commit to JNyyyyy/noop that referenced this pull request Jul 20, 2026
…) (ryanbr#243)

* Reboot: mark the WHOOP 4.0 frame UNVERIFIED (it's ignored on 4.0) (ryanbr#235)

A 4.0 owner's strap log (ryanbr#235) shows the Restart command does nothing on a real 4.0:
`reboot: sent opcode=29 framing=harvard-crc8 payload=empty` → no COMMAND_RESPONSE,
no disconnect, `no disconnect within 12s — strap may have ignored the command`, and
the strap keeps streaming normally. The empty-body harvard frame was decoded from the
app builder but NEVER hardware-verified; a real 4.0 silently ignores it. (Only the 5.0
puffin form is confirmed — ryanbr#227.)

Honesty patch — flips the labelling that had this backwards. No behaviour change; the
correct 4.0 frame still needs an HCI capture, so nothing is guessed here:
- framing log: 4.0 now "harvard-crc8 (UNVERIFIED on 4.0)" (5.0 unchanged);
- no-disconnect watchdog: 4.0 branch now points at ryanbr#235 (was empty);
- Restart confirm dialog: was "On WHOOP 5.0/MG this is experimental" — reversed to
  "confirmed on 5.0; on 4.0 the reboot isn't confirmed yet";
- Commands.swift / Enums.kt / PROTOCOL.md comments corrected (they claimed 4.0
  confirmed + 5/MG unverified — both backwards now).

Both platforms, byte-identical strings. Android compileFullDebugKotlin passes.

* Reboot: gated WHOOP 4.0 reboot probe to find the real frame (ryanbr#235)

The honesty patch (previous commit) stops the app claiming the 4.0 reboot works, but the
strap still won't reboot — the correct 4.0 frame is unknown. This adds an opt-in, gated
probe to find it on real hardware, the same way the alarm frame was pinned (@ujix's 4.0
btsnoop capture, ryanbr#535).

Test Centre → Connection, WHOOP 4.0 only: a "Reboot probe (4.0 RE)…" action in the device
menu sends one non-destructive candidate frame at a time and reuses the ryanbr#216 reboot
watchdog, so the strap log shows which one drops the link (`reboot: link dropped …` = it
worked) vs is ignored (`reboot: no disconnect within 12s …`):
  A · REBOOT_STRAP(29) empty       — current production frame (ignored on 4.0)
  B · POWER_CYCLE(32) empty        — a harder restart, never tried
  C · REBOOT_STRAP(29) payload=01  — same opcode with a non-empty sub-command byte

- New `RebootProbeVariant` (Swift Commands.swift / Kotlin Enums.kt), byte-identical logTags.
- `POWER_CYCLE_STRAP` (32) added to the command enum as a SECOND guarded exception: a
  restart is non-destructive (data lives in flash), sent ONLY via rebootProbe(), 4.0-only,
  confirmation-gated, never automatic. FORCE_TRIM / firmware / DFU / fuel-gauge stay excluded.
- `rebootStrap()` refactored to a shared `sendRebootFrame()`; `rebootProbe()` reuses it.
- UI gated: menu entry + a candidate-list dialog appear only with Connection test mode on
  and a live 4.0 connected. A 5.0 already reboots on the production frame (ryanbr#227), so nothing
  to probe there — the backend also hard-guards family == WHOOP4.
- PROTOCOL.md documents the probe + the two guarded restart exceptions.

Both platforms. Android compileFullDebugKotlin + protocol tests pass; app-target Swift via
app-build. On-device: no 4.0 to hand yet — the probe is the tool to run once one is.

* Reboot probe: also log the COMMAND_RESPONSE for POWER_CYCLE_STRAP (ryanbr#235)

Re-review catch. The `reboot: strap acked result=…` handler matched only REBOOT_STRAP, so
the probe's candidate B (POWER_CYCLE_STRAP/32) would drop its COMMAND_RESPONSE on the floor.
That result byte is exactly the datum that distinguishes "opcode recognised but rejected"
(recognised, wrong args — like the 5/MG haptics result=0x03) from "silently ignored" (wrong
opcode entirely) — the whole point of trying candidate B. Match POWER_CYCLE_STRAP too, both
platforms (FrameRouter.swift prefix match + WhoopBleClient resp_cmd startsWith). Log-only.

* Reboot probe: derive logged opcode from the command; pin the candidate table (ryanbr#235)

Re-review follow-up. RebootProbeVariant carried a standalone `opcode` field that duplicated
`command.rawValue` — two sources of truth that had to be kept in sync, and a divergence would
log one opcode while sending another. Drop it; sendRebootFrame now derives the logged opcode
from the command's on-wire value on both platforms, so the log can't disagree with the bytes.

Add RebootProbeVariantTest (JVM) pinning the candidate table — command, payload hex, menuLabel,
and the byte-identical logTags — as the parity anchor the Swift RebootProbeVariant must match.
DX23876 pushed a commit to DX23876/noop that referenced this pull request Jul 22, 2026
…) (ryanbr#243)

* Reboot: mark the WHOOP 4.0 frame UNVERIFIED (it's ignored on 4.0) (ryanbr#235)

A 4.0 owner's strap log (ryanbr#235) shows the Restart command does nothing on a real 4.0:
`reboot: sent opcode=29 framing=harvard-crc8 payload=empty` → no COMMAND_RESPONSE,
no disconnect, `no disconnect within 12s — strap may have ignored the command`, and
the strap keeps streaming normally. The empty-body harvard frame was decoded from the
app builder but NEVER hardware-verified; a real 4.0 silently ignores it. (Only the 5.0
puffin form is confirmed — ryanbr#227.)

Honesty patch — flips the labelling that had this backwards. No behaviour change; the
correct 4.0 frame still needs an HCI capture, so nothing is guessed here:
- framing log: 4.0 now "harvard-crc8 (UNVERIFIED on 4.0)" (5.0 unchanged);
- no-disconnect watchdog: 4.0 branch now points at ryanbr#235 (was empty);
- Restart confirm dialog: was "On WHOOP 5.0/MG this is experimental" — reversed to
  "confirmed on 5.0; on 4.0 the reboot isn't confirmed yet";
- Commands.swift / Enums.kt / PROTOCOL.md comments corrected (they claimed 4.0
  confirmed + 5/MG unverified — both backwards now).

Both platforms, byte-identical strings. Android compileFullDebugKotlin passes.

* Reboot: gated WHOOP 4.0 reboot probe to find the real frame (ryanbr#235)

The honesty patch (previous commit) stops the app claiming the 4.0 reboot works, but the
strap still won't reboot — the correct 4.0 frame is unknown. This adds an opt-in, gated
probe to find it on real hardware, the same way the alarm frame was pinned (@ujix's 4.0
btsnoop capture, ryanbr#535).

Test Centre → Connection, WHOOP 4.0 only: a "Reboot probe (4.0 RE)…" action in the device
menu sends one non-destructive candidate frame at a time and reuses the ryanbr#216 reboot
watchdog, so the strap log shows which one drops the link (`reboot: link dropped …` = it
worked) vs is ignored (`reboot: no disconnect within 12s …`):
  A · REBOOT_STRAP(29) empty       — current production frame (ignored on 4.0)
  B · POWER_CYCLE(32) empty        — a harder restart, never tried
  C · REBOOT_STRAP(29) payload=01  — same opcode with a non-empty sub-command byte

- New `RebootProbeVariant` (Swift Commands.swift / Kotlin Enums.kt), byte-identical logTags.
- `POWER_CYCLE_STRAP` (32) added to the command enum as a SECOND guarded exception: a
  restart is non-destructive (data lives in flash), sent ONLY via rebootProbe(), 4.0-only,
  confirmation-gated, never automatic. FORCE_TRIM / firmware / DFU / fuel-gauge stay excluded.
- `rebootStrap()` refactored to a shared `sendRebootFrame()`; `rebootProbe()` reuses it.
- UI gated: menu entry + a candidate-list dialog appear only with Connection test mode on
  and a live 4.0 connected. A 5.0 already reboots on the production frame (ryanbr#227), so nothing
  to probe there — the backend also hard-guards family == WHOOP4.
- PROTOCOL.md documents the probe + the two guarded restart exceptions.

Both platforms. Android compileFullDebugKotlin + protocol tests pass; app-target Swift via
app-build. On-device: no 4.0 to hand yet — the probe is the tool to run once one is.

* Reboot probe: also log the COMMAND_RESPONSE for POWER_CYCLE_STRAP (ryanbr#235)

Re-review catch. The `reboot: strap acked result=…` handler matched only REBOOT_STRAP, so
the probe's candidate B (POWER_CYCLE_STRAP/32) would drop its COMMAND_RESPONSE on the floor.
That result byte is exactly the datum that distinguishes "opcode recognised but rejected"
(recognised, wrong args — like the 5/MG haptics result=0x03) from "silently ignored" (wrong
opcode entirely) — the whole point of trying candidate B. Match POWER_CYCLE_STRAP too, both
platforms (FrameRouter.swift prefix match + WhoopBleClient resp_cmd startsWith). Log-only.

* Reboot probe: derive logged opcode from the command; pin the candidate table (ryanbr#235)

Re-review follow-up. RebootProbeVariant carried a standalone `opcode` field that duplicated
`command.rawValue` — two sources of truth that had to be kept in sync, and a divergence would
log one opcode while sending another. Drop it; sendRebootFrame now derives the logged opcode
from the command's on-wire value on both platforms, so the log can't disagree with the bytes.

Add RebootProbeVariantTest (JVM) pinning the candidate table — command, payload hex, menuLabel,
and the byte-identical logTags — as the parity anchor the Swift RebootProbeVariant must match.
DX23876 pushed a commit to DX23876/noop that referenced this pull request Jul 22, 2026
A release pushes STRAIGHT to main with no PR, and appchangelog-gen.py emits the
What's New title as a RAW Kotlin literal that has to be localized by hand
afterwards. With a PR-only trigger that landed completely unaudited — and
because --ci is a whole-tree gate rather than a diff, it then red-checked EVERY
open PR on a line none of them touched. That is exactly what 9.0.1 did (fixed
by ryanbr#514): the break was invisible where it was introduced and highly visible
everywhere it wasn't.

Audit pushes to main so a release-introduced literal surfaces on main the
moment it lands, and gets fixed before any contributor trips over it. Same
8-second ubuntu job, no new cost.

github.base_ref is set only for pull_request and is empty on a push (which
would pass a bare 'origin/'), so fall back to main. The ref is retained for
workflow compatibility — --ci is a whole-tree gate, so the resolved value does
not change the verdict.

YAML parses (both triggers); the push path's exact command
'i18n_audit.py --ci origin/main' passes on current main.
@ryanbr
ryanbr deleted the fix/i18n-audit-on-main-push branch August 2, 2026 07:41
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…) (ryanbr#243)

* Reboot: mark the WHOOP 4.0 frame UNVERIFIED (it's ignored on 4.0) (ryanbr#235)

A 4.0 owner's strap log (ryanbr#235) shows the Restart command does nothing on a real 4.0:
`reboot: sent opcode=29 framing=harvard-crc8 payload=empty` → no COMMAND_RESPONSE,
no disconnect, `no disconnect within 12s — strap may have ignored the command`, and
the strap keeps streaming normally. The empty-body harvard frame was decoded from the
app builder but NEVER hardware-verified; a real 4.0 silently ignores it. (Only the 5.0
puffin form is confirmed — ryanbr#227.)

Honesty patch — flips the labelling that had this backwards. No behaviour change; the
correct 4.0 frame still needs an HCI capture, so nothing is guessed here:
- framing log: 4.0 now "harvard-crc8 (UNVERIFIED on 4.0)" (5.0 unchanged);
- no-disconnect watchdog: 4.0 branch now points at ryanbr#235 (was empty);
- Restart confirm dialog: was "On WHOOP 5.0/MG this is experimental" — reversed to
  "confirmed on 5.0; on 4.0 the reboot isn't confirmed yet";
- Commands.swift / Enums.kt / PROTOCOL.md comments corrected (they claimed 4.0
  confirmed + 5/MG unverified — both backwards now).

Both platforms, byte-identical strings. Android compileFullDebugKotlin passes.

* Reboot: gated WHOOP 4.0 reboot probe to find the real frame (ryanbr#235)

The honesty patch (previous commit) stops the app claiming the 4.0 reboot works, but the
strap still won't reboot — the correct 4.0 frame is unknown. This adds an opt-in, gated
probe to find it on real hardware, the same way the alarm frame was pinned (@ujix's 4.0
btsnoop capture, ryanbr#535).

Test Centre → Connection, WHOOP 4.0 only: a "Reboot probe (4.0 RE)…" action in the device
menu sends one non-destructive candidate frame at a time and reuses the ryanbr#216 reboot
watchdog, so the strap log shows which one drops the link (`reboot: link dropped …` = it
worked) vs is ignored (`reboot: no disconnect within 12s …`):
  A · REBOOT_STRAP(29) empty       — current production frame (ignored on 4.0)
  B · POWER_CYCLE(32) empty        — a harder restart, never tried
  C · REBOOT_STRAP(29) payload=01  — same opcode with a non-empty sub-command byte

- New `RebootProbeVariant` (Swift Commands.swift / Kotlin Enums.kt), byte-identical logTags.
- `POWER_CYCLE_STRAP` (32) added to the command enum as a SECOND guarded exception: a
  restart is non-destructive (data lives in flash), sent ONLY via rebootProbe(), 4.0-only,
  confirmation-gated, never automatic. FORCE_TRIM / firmware / DFU / fuel-gauge stay excluded.
- `rebootStrap()` refactored to a shared `sendRebootFrame()`; `rebootProbe()` reuses it.
- UI gated: menu entry + a candidate-list dialog appear only with Connection test mode on
  and a live 4.0 connected. A 5.0 already reboots on the production frame (ryanbr#227), so nothing
  to probe there — the backend also hard-guards family == WHOOP4.
- PROTOCOL.md documents the probe + the two guarded restart exceptions.

Both platforms. Android compileFullDebugKotlin + protocol tests pass; app-target Swift via
app-build. On-device: no 4.0 to hand yet — the probe is the tool to run once one is.

* Reboot probe: also log the COMMAND_RESPONSE for POWER_CYCLE_STRAP (ryanbr#235)

Re-review catch. The `reboot: strap acked result=…` handler matched only REBOOT_STRAP, so
the probe's candidate B (POWER_CYCLE_STRAP/32) would drop its COMMAND_RESPONSE on the floor.
That result byte is exactly the datum that distinguishes "opcode recognised but rejected"
(recognised, wrong args — like the 5/MG haptics result=0x03) from "silently ignored" (wrong
opcode entirely) — the whole point of trying candidate B. Match POWER_CYCLE_STRAP too, both
platforms (FrameRouter.swift prefix match + WhoopBleClient resp_cmd startsWith). Log-only.

* Reboot probe: derive logged opcode from the command; pin the candidate table (ryanbr#235)

Re-review follow-up. RebootProbeVariant carried a standalone `opcode` field that duplicated
`command.rawValue` — two sources of truth that had to be kept in sync, and a divergence would
log one opcode while sending another. Drop it; sendRebootFrame now derives the logged opcode
from the command's on-wire value on both platforms, so the log can't disagree with the bytes.

Add RebootProbeVariantTest (JVM) pinning the candidate table — command, payload hex, menuLabel,
and the byte-identical logTags — as the parity anchor the Swift RebootProbeVariant must match.
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
A release pushes STRAIGHT to main with no PR, and appchangelog-gen.py emits the
What's New title as a RAW Kotlin literal that has to be localized by hand
afterwards. With a PR-only trigger that landed completely unaudited — and
because --ci is a whole-tree gate rather than a diff, it then red-checked EVERY
open PR on a line none of them touched. That is exactly what 9.0.1 did (fixed
by ryanbr#514): the break was invisible where it was introduced and highly visible
everywhere it wasn't.

Audit pushes to main so a release-introduced literal surfaces on main the
moment it lands, and gets fixed before any contributor trips over it. Same
8-second ubuntu job, no new cost.

github.base_ref is set only for pull_request and is empty on a push (which
would pass a bare 'origin/'), so fall back to main. The ref is retained for
workflow compatibility — --ci is a whole-tree gate, so the resolved value does
not change the verdict.

YAML parses (both triggers); the push path's exact command
'i18n_audit.py --ci origin/main' passes on current main.
ryanbr pushed a commit that referenced this pull request Aug 28, 2026
…are actually uses (#1713)

Contributed by @Zebsi235, with three arm/readback captures from a WHOOP 4.0 on fw 41.17.6.0.

The GET readback is 11 bytes and carries a stored flag the 9-byte SET payload does not, so its epoch
sits at offset 2:

  SET  (9):  [form 01][u32 LE epoch][00 00 00 00]
  GET (11):  [form 01][stored flag][u32 LE epoch][00 00][04 00 20]

Decoding it at the SET-mirror offset returned the epoch's low three bytes shifted up a byte plus the
flag — wrong by roughly 256x and free to land anywhere in u32 range. In all three captures it landed
on a 2045 date INSIDE the 2017..2100 plausibility window, so the gate accepted it and a strap whose
alarm register was working perfectly was reported as refusing the time.

Every number re-derived from the raw payloads before merging. The u32-LE at offset 2 is the sent
epoch byte-exact in all three; the u32-LE at offset 1 is exactly what the app logged in all three;
the misreads step by 22,118,400, which is 86400 x 256 — one day of arm, shifted a byte. Three
independent epochs, no misses.

Why it hid: the only fixture in the tree, the #34 field report 01 00 00 00 00 00 00 00 04 00 20, is
this same shape with the flag 00 and the epoch zeroed — and a zeroed epoch region decodes identically
at offsets 1 and 2. The existing test could not have caught this, and did not.

The decode tries the 11-byte shape first and, on an implausible epoch, returns nil to the raw-hex line
rather than falling through to the mirror offsets: those are provably wrong for this shape, so trying
them could resurrect the misread. readbackReportsNoAlarm takes the same ordering, so both functions
read the same shapes in the same order their docs promise, and the length gate keeps the 9-byte #535
capture structurally unreachable from the new branch. Kotlin twin is logically identical; four new
tests per platform, all three captures pinned on both, no existing test touched.

This also changes what earlier reports meant: a pre-fix reported epoch from a 4.0 readback of this
shape cannot be trusted whatever it looked like, sane-looking values included, because an offset-1
read that fell outside the window dropped through to a bare-u32 read of different bytes. #1707 stopped
the cross-strap comparison from accusing the wrong device; this stops the same-strap one from being
wrong as well.

Left alone deliberately, and correctly: the 82-year plausibility window, since on firmwares answering
with some other shape the wide window plus raw-hex fallback is still what gets the bytes captured. And
the 2045 value from the OTHER install in #1706 is not explained here — its low byte is 0xD4, which
cannot be this shape's flag, so that one still needs its own capture.
UtkuDenizAltiok added a commit to UtkuDenizAltiok/noop that referenced this pull request Nov 6, 2026
The strap wake-alarm's two descriptions on the Alarms screen read "Sends the exact alarm command the
official app sends, confirmed buzzing on a real WHOOP 4.0 (community wire capture + on-device test,
ryanbr#535)". Simple mode keeps every fact a wearer needs (it buzzes even with NOOP closed or the phone
asleep, it is confirmed on WHOOP 4.0, keep a backup alarm) and drops the capture method and the issue
number. The experimental 5/MG description and the full view are unchanged. 2 new strings, nine locales.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant