ci(i18n): audit main pushes, not just PRs into it - #535
Merged
Conversation
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A release pushes straight to
mainwith no PR, andTools/appchangelog-gen.pyemits 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--ciis 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
maintoo, so a release-introduced literal surfaces on main the moment it lands and gets fixed before any contributor trips over it.github.base_refis set only forpull_requestand is empty on a push (which would pass a bareorigin/), so it falls back tomain. The ref is retained for workflow compatibility —--ciis 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
pull_request+push, both[main]).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-gencan't fully self-solve: it could emit theuiString(R.string.…)key + theenstring, but it can't translate, sode/es/frwould be missing and the coverage half would still fail — the localize step stays manual, which is precisely why it needs a guard. Gatingfork-release.ymlon the audit would be the stronger (release-blocking) step; deliberately left as a separate call.