• v2.0.25 1655d4e2e1

    thoralf released this 2026-10-23 06:56:20 +02:00 | 4531 commits to main since this release

    Same fixes as dev 2.0.81 — the upstream-merge model and the release-pipeline fixes of 2026-10-18/19.

    Fixed

    • The F-Droid "What's New" can no longer be blocked by a changelog entry that mentions
      a heading.
      The converter stripped backticks and bold but left a literal ### in the
      text, so the release gate's "no Markdown left" assertion failed — and that is exactly
      what happened to 2.0.81 on the dev channel: the entry about the duplicate ### Changed
      heading fed its own heading into the What's New text, the gate refused, and the release
      was never signed (dev stayed on 2.0.80). The converter now drops heading hash runs (the
      words stay, so the entry still reads correctly), which makes its guarantee hold for any
      input. prod was not affected.
    • merge=ours can no longer silently discard your change. The fork-owned files
      (README, CHANGELOG, LICENSE, SECURITY, PRIVACY) must keep our version on an upstream
      merge — but git calls that driver for every three-way merge, and on a rebase or
      cherry-pick "ours" is the branch being replayed onto while "theirs" is your commit.
      The built-in true therefore threw your change away, silently: it did exactly that
      three times on 2026-10-18/19 (both prod cherry-picks and one rebase, each losing a
      CHANGELOG entry), with no conflict and nothing in the diffstat to catch the eye.
      scripts/phandroid-merge-ours.sh replaces it: it still keeps ours, but writes the
      discarded version to .git/phandroid-dropped/<path> and names it on stderr, so the
      loss is visible while it happens and recoverable afterwards. It needs the driver once
      per clone (git config merge.phandroid-ours.driver …; the workflow declares it for
      CI), and a clone without it now gets a visible conflict instead of silence.
    • The upstream sync no longer re-resolves the lockfile from a stale base — the
      outage class that stranded dev at 2.0.79 for two days is closed.

      package-lock.json is now merge=theirs: a merge takes upstream's lock, the
      dependency resolution they actually build and test with, and the sync
      regenerates it only to re-add the fork's eleven overlay entries. The old model
      kept our lock (merge=ours) and re-resolved it from scratch on every merge,
      handing each upstream dependency bump to npm's peer resolver as a fresh coin
      flip: on 2026-10-16 upstream bumped @lingui to ~6.9.1 and npm walked the
      unguarded peerOptional chain (@lingui/vite-plugin →
      @rolldown/plugin-babel → @babel/plugin-transform-runtime) into Babel 8.x
      against the tree's @babel/[email protected] — ERESOLVE, and the sync failed
      eleven consecutive runs while dev sat 20 commits behind upstream. Starting from
      upstream's lock instead, the identical regeneration touches nothing upstream
      owns (measured: 716 entries, 0 changed, 0 removed, 195 added by the overlay;
      verified under npm 9.2 and 11.21). A new guard,
      scripts/phandroid-lockfile-invariant.mjs, fails the run if the regeneration
      changes or drops any upstream-owned entry — the drift can neither crash the
      sync nor ship silently again. This branch carries the same model and the same
      guard, so the next upstream/production move cannot hit the same wall.
    • The lockfile guard fails closed. Input it cannot check — a lockfile without a
      packages map (lockfileVersion 1), an unparsable or missing file — now stops the
      run with one line naming the file and the reason (exit 2), instead of comparing
      zero entries and reporting a green "0 upstream entries unchanged". A guard that
      validates nothing is the failure mode this pipeline keeps hitting: an unmatched
      test glob ran zero tests and exited 0, and the awk the changelog bump replaced
      exited 0 on a missing [Unreleased] heading. Only exit 0 means the invariant held.
    • A release section no longer carries two ### Changed headings. The generated
      upstream-merge block was followed by the moved hand-written entries including
      their own ### Changed, so a release whose entries had one showed that heading
      twice — in the release body on both instances (2.0.80 shipped that way) and, for
      sections short enough to survive the 500-character limit, twice as "Changed:" in
      the F-Droid "What's New" text (measured against the pre-fix script; in 2.0.80 the
      truncation dropped the second block, so there only the release body showed it).
      Hand-written ### Changed bullets now merge into the generated block, which stays
      first because the upstream merge is the release's headline; every other
      hand-written section keeps its place. A body without a ### Changed heading still
      produces byte-identical output — verified against the pre-fix script over six
      input shapes. A heading spelled with extra whitespace (### Changed) counts as
      the same heading, and a body of nothing but headings no longer counts as fork
      changes: it used to append a stray empty heading and drop the "no Android-fork
      code changes" claim for a release that had none.

    Changed

    • A failed sync now says why, and for how long it has been failing. The
      lockfile step tees its output into the failure report — the ERESOLVE that
      killed eleven runs was previously only findable by pulling job logs by hand —
      a failed merge lists its conflicted files the same way, and every CI-failure
      issue now carries a trend line ("N consecutive failed run(s); last success:
      run X on …") computed from the run history. Forgejo notifies nobody about
      Actions results, so the issue body is the only alarm channel — and "still
      down" reads differently than "another failure".
    • The sync announces conflict candidates before merging. Files that both
      upstream and the fork changed since the last merge — minus the ones the merge
      drivers own — are printed up front, so the next real conflict shows up in the
      job log and the issue instead of being a surprise mid-merge.
    • The conflict-candidate list is read from .gitattributes instead of a fixed
      list of files.
      Upstream edits its own CHANGELOG.md regularly and merge=ours
      resolves that without a human, so the hardcoded version would have announced a
      conflict candidate on most runs — the noise that makes a real one invisible.
    Downloads