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 hourTwo 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 runWhat 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.