Skip to content

Fix #1305: reject stale tier-1 version.json below v2.0 - #1321

Closed
rumblefrog wants to merge 1 commit into
mainfrom
fix/issue-1305-version-floor
Closed

rumblefrog wants to merge 1 commit into
mainfrom
fix/issue-1305-version-floor

Conversation

@rumblefrog

Copy link
Copy Markdown
Member

Summary

Sbpp\Version::resolve() previously trusted any configs/version.json
verbatim. The v1.x repo carried that file as a checked-in, hand-edited
literal ({"version": "1.8.1", "git": "1434"}) until #1070 deleted it
and made the release pipeline its sole writer. An operator who
upgraded a v1.8 install to v2.0 with a "skip if exists" overlay tool
(FTP, rsync without --delete, manual directory copy that treats
configs/ as user data alongside configs/permissions/*.json) keeps
the stale file on disk; the resolver returned its contents verbatim
and the chrome footer read SourceBans++ 1.8.1 | Git: 1434 on a v2.0
install (with data-version=\"1.8.1\" poisoning telemetry, bug
reports, and E2E specs that key off the attribute).

This PR ships the issue's suggested fix (2) — resolver-side floor
check — paired with the doc-only fix (3) for already-upgraded
in-the-wild installs. It does not ship the migration-side fix (1):
the migration could only delete the stale file, which would
regress correctly-overlaid installs (the new version.json would
be wiped and the footer would read dev); the resolver-side check
fails closed for the broken case without harming the working case.

  • Add Version::MIN_TIER1_MAJOR = 2 and gate the tier-1 input on
    it via a new isAcceptableTier1Version() helper. A version
    whose major component is below the floor falls through to tier-2
    (git describe) or tier-3 (the 'dev' sentinel) — the operator
    sees a self-describing fallback instead of phantom v1.x copy.
  • Major-only comparison (vs. a strict version_compare(..., '2.0.0', '>=')) is deliberate so pre-release tarballs like 2.0.0-rc.1 —
    which sort BELOW 2.0.0 under semver's "pre-release < release"
    rule — stay accepted. The constant's docblock calls out the
    "bump on every MAJOR release" maintenance burden, paired with a
    testFloorConstantIsTwo() regression guard so the bump becomes
    a deliberate test edit.
  • Pin every reject branch (stale v1.x with git available + with git
    absent) and every accept branch (boundary 2.0.0, pre-release
    2.0.0-rc.1, future 3.0.0, plus a malformed-version data
    provider with six edge cases) in web/tests/unit/VersionTest.php
    — bringing the suite from 6 to 18 tests, all passing.
  • Document the v2.0.0 file-ownership contract (and the recovery
    procedure for already-upgraded installs) in UPGRADING.md; cross-
    link the floor from AGENTS.md "Where to find what" and
    ARCHITECTURE.md's init.php lifecycle step 5.

Fixes #1305.

Test plan

  • ./sbpp.sh test tests/unit/VersionTest.php — 18 / 18 pass
    (6 pre-existing + 12 new across testStaleV1JsonFallsThroughTo*,
    testTarballJsonAtFloorMajorIsAccepted,
    testTarballJsonPreReleaseWithinCurrentMajorIsAccepted,
    testTarballJsonFutureMajorIsAccepted, the
    provideMalformedTier1Versions data provider, and
    testFloorConstantIsTwo).
  • ./sbpp.sh test — 415 / 415 pass (the suite-wide PHPUnit
    deprecation that remains is GroupsTest::testEditRoundTripsHighBitFlagsAsPositiveIntegers's
    @dataProvider doc-comment, pre-existing and out of scope).
  • ./sbpp.sh phpstan — clean (no errors at level 5 + dba).
  • No web/scripts/, web/themes/*/js/, or web/api/handlers/
    touched, so ts-check, composer api-contract, and e2e are
    not in the diff path. The chrome footer template
    (web/themes/default/core/footer.tpl) still mirrors SB_VERSION
    unchanged; the only behaviour change is what SB_VERSION resolves
    to when configs/version.json carries a stale v1.x version.

Reviewer notes

  • The migration-side fix (issue option 1) was deliberately skipped
    — see Summary for the regression-on-correctly-deployed-installs
    reasoning. The resolver-side check + the doc note covers the
    failure mode without harming the working case.
  • The floor needs to be bumped to 3 when v3.0 ships;
    testFloorConstantIsTwo() is the paired regression guard so
    forgetting to bump it shows up as a failing test rather than a
    silently re-opened bug. AGENTS.md's "Where to find what" row
    carries the same note.

`Sbpp\Version::resolve()` previously trusted any `configs/version.json`
verbatim. The v1.x repo carried that file as a checked-in, hand-edited
literal (`{"version": "1.8.1", "git": "1434"}`) until #1070 deleted it
and made the release pipeline its sole writer. An operator who upgraded
v1.8 → v2.0 with a "skip if exists" overlay tool (FTP, `rsync` without
`--delete`, manual directory copy that treats `configs/` as user data)
keeps the stale file on disk; the resolver returned its contents
verbatim and the chrome footer read `SourceBans++ 1.8.1 | Git: 1434` on
a v2.0 install (with `data-version="1.8.1"` poisoning telemetry, bug
reports, and E2E specs that key off the attribute).

Add a `Version::MIN_TIER1_MAJOR = 2` floor and gate the tier-1 input
on it via a new `isAcceptableTier1Version()` helper. A version whose
major component is below the floor falls through to tier-2 (`git
describe`) or tier-3 (the `'dev'` sentinel) — the operator sees a
self-describing fallback instead of phantom v1.x copy.

Major-only comparison (vs. a strict `version_compare(..., '2.0.0',
'>=')`) is deliberate so pre-release tarballs like `2.0.0-rc.1` —
which sort BELOW `2.0.0` under semver's "pre-release < release" rule
— stay accepted. The constant carries a docblock that calls out the
"bump on every MAJOR release" maintenance burden, paired with a
`testFloorConstantIsTwo()` regression guard so the bump becomes a
deliberate test edit.

Pin every reject branch (stale v1.x with git available + with git
absent) and every accept branch (boundary `2.0.0`, pre-release
`2.0.0-rc.1`, future `3.0.0`, plus a malformed-version data provider
with six edge cases) in `web/tests/unit/VersionTest.php`. Document
the v2.0.0 file-ownership contract (and the recovery procedure for
already-upgraded installs) in `UPGRADING.md`; cross-link the floor
from `AGENTS.md` "Where to find what" and `ARCHITECTURE.md`'s
init.php lifecycle step 5.

Fixes #1305.
@rumblefrog rumblefrog closed this May 10, 2026
@rumblefrog

Copy link
Copy Markdown
Member Author

Not a bug, the user was testing straight from main. On normal path, the release will contain CI generated version.json. We don't need to support other paths of cloning from main.

@rumblefrog
rumblefrog deleted the fix/issue-1305-version-floor branch May 13, 2026 05:08
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.

Footer shows stale v1.8.x version after upgrade to v2.0 (resolver returns preserved web/configs/version.json verbatim)

1 participant