A licence that came back, and the migration we had prepared

In April 2024 a dependency changed to a source-available licence, we read it, decided it did not restrict us, pinned the version and kept an option open. On the first of May 2025 it shipped under AGPLv3 and became open source again, which is the one outcome the decision record did not anticipate.

The symptom

thirteen months of a decision held open:

  a version pinned to 7.2.4
  a monthly job running 41 integration tests against
    both the original and the fork
  a decision record with four triggers, none of which
    fired
  a review scheduled for October 2024, held, and
    concluded "no change"

cost: about twenty minutes of runner time a year, and
a pin that had to be defended at every review.

A pinned version is a decision that has to be re-justified rather than a state that persists quietly, which is the ongoing cost of holding an option open. Thirteen months is long enough that the pressure to just migrate and stop thinking about it had become the strongest argument in the room.

Why it happens

A licence change is a business decision and so is reverting one. Neither is predictable from outside, which means any plan that assumes a direction is a bet — and the useful plans are the ones that are cheap regardless of which way it goes.

The fix

Reading AGPL rather than assuming it

the clause with the reputation: if you MODIFY the
software and let users interact with the modified
version over a network, you must offer them the source
of your modifications.

our situation:
  we run an unmodified release
  our users interact with our application; the cache is
    behind it and is not reachable from outside

so: nothing is required.

the case that WOULD matter, written down as a trigger:
  patching it, or exposing it directly to a third
  party.

The obligation attaches to modification and to network interaction with the modified work, and running an unmodified binary behind an application triggers neither. Writing down which of the two would have to change is more actionable than a general anxiety about a three-letter licence, and it is the trigger for revisiting.

What thirteen months of preparation actually bought

  the compatibility suite      41 tests, 14 runs, 0
                               failures. proved the
                               fork was a viable
                               target.
  the pin                      prevented an accidental
                               upgrade to a
                               source-available
                               version. genuinely
                               useful.
  the decision record          made the October review
                               a fifteen-minute read.

and what it did not buy: any of the migration work,
because the migration never happened.

honest assessment: the pin was worth it. the
compatibility suite was worth it as tests, and not as
a hedge.

The tests turned out to be the durable artefact and the dual-server run was the temporary part, which is the right way round and was not the original intention. Keeping them and retiring the second server is what a resolved option looks like.

The upgrade

$ git diff docker-compose.yml
-  image: redis:7.2.4@sha256:8c1f4a7e...
+  image: redis:8.0@sha256:4a7e8c1f...

$ docker compose up -d cache
$ vendor/bin/phpunit --group=cache
  Tests: 41 passed

$ ./bin/cache-hit-rate --since=1h
  0.00%    # correct: a new container is empty
$ ./bin/cache-hit-rate --since=24h
  91.2%    # and back to normal within the hour

Two major versions in one step, from 7.2 to 8.0, with no configuration change and no application change — which is what a compatibility suite running monthly for thirteen months buys in confidence rather than in code. The empty cache after the restart is the only visible effect and it resolves on its own.

Keeping the fork installed

the compatibility job retired, and the fork kept in
the local development stack as an optional profile.

  docker compose --profile fork up -d

why: the option is now a preference rather than an
escape route, and the cost of keeping it discoverable
is a profile nobody starts.

and the review date: May 2026, to decide whether even
that is worth keeping.

Closing the decision record

# ADR-023: Stay on the current cache server

## Status
**Closed, 2025-05-06.** The licence change that
prompted this was reverted upstream on 2025-05-01.

## Outcome
None of the four triggers fired. A fifth situation —
the upstream reverting — was not anticipated and is the
one that occurred.

## What we would do again
Pin, and write down the triggers. Thirteen months of a
monthly job is a cheap option.

## What we would do differently
The review dates were six months apart. Three months
would have been the same cost and would have caught
the revert three weeks earlier, which changed nothing.

A closed record rather than a deleted one is what makes the next licence change a shorter conversation, and the “what we would do differently” section is the only part with any predictive value. Admitting that the earlier review would have changed nothing is the honest version — it is the kind of process improvement that feels responsible and buys nothing.

Verifying it worked

$ docker compose exec cache redis-server --version
  Redis server v=8.0.1

$ grep -c 'sha256:' docker-compose.yml
4        # still pinned by digest, which is unrelated
         # to the licence and is about reproducibility

$ gh workflow list | grep -c cache-compat
0        # retired

$ vendor/bin/phpunit --group=cache
  Tests: 41 passed        # now part of the ordinary run

What this costs

Thirteen months of a scheduled job and a pin that needed defending at every review, to preserve an option that resolved on its own. That is the cost of preparing for something that does not happen, and it is the correct cost — the alternative was migrating in 2024 under time pressure to escape a restriction that never applied.

It also establishes a habit that is worth watching. Preparing for every upstream decision this way does not scale — there are a dozen dependencies that could do this, and holding an option open for each is a dozen monthly jobs. The reason this one earned it is that it was in the data path and had no equivalent, which is a test the next case will have to pass.