Skip to content

feat(publication): releases, a cross-locale identifier for a publish moment - #680

Open
midego1 wants to merge 1 commit into
unopim:3.xfrom
midego1:feat/publication-releases
Open

midego1 wants to merge 1 commit into
unopim:3.xfrom
midego1:feat/publication-releases

Conversation

@midego1

@midego1 midego1 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Problem

Versions are numbered per locale, so "version 3" names a different state in every language. There is nothing that identifies one publish moment across locales.

That matters for the Digital Product Passport: the state a product was placed on the market with is one moment, not one per language, and a printed carrier needs one number that keeps meaning that moment. Today the only cross-locale handle is a timestamp lookup, which is nothing you can print or reason about.

Change

New table publication_releases: publication_id, sequence (monotonic per publication, unique on (publication_id, sequence)), published_at, published_by_id. One release is minted per minted version, inside the same publication row lock that already protects the version number, so MAX(sequence) + 1 cannot race for the same reason MAX(version) + 1 cannot.

publication_versions.release_id (nullable FK, restrictOnDelete), set by both publish() and republishFrom(). The version's published_at is now taken from the release so the two never drift.

Backfill in the same migration: every existing version becomes its own release, numbered per publication in (published_at, id) order. Uses the query builder only, so it runs on MySQL and PostgreSQL alike and respects the table prefix.

Immutability: releases refuse update() and delete() with ImmutableVersionException, like versions. release_id is not in a version's mutable-after-publish set, so moving a version to another release throws.

PublicationRelease::versionsAsOf(): the state of the publication as of that release, for every locale the most recent version minted at or before it, keyed by locale_id. Redacted versions are included; rendering them is the caller's decision. This is the read a per-release public route needs and nothing else.

No public behaviour changes in this PR. Routes, templates and JSON-LD are untouched.

Tests

  • one release per minted version, numbered per publication; an unchanged publish mints neither
  • releases number across locales in publish order, and versionsAsOf() resolves the right version per locale for each of three consecutive releases, while version numbers keep running per locale
  • republishFrom() mints a release too
  • a release refuses update and delete
  • a version refuses to move to another release

Backfill was additionally exercised by rolling the migration back on a database with interleaved versions across two publications and migrating forward again: sequences come out per publication in publish order, and no release_id is left null.

Related

Follows #677, #678 and #679, which make a sealed version's documents and redaction trustworthy. A per-release public route builds on this.

…moment

Versions are numbered per locale, so "version 3" names a different
state in every language. Nothing identifies one moment across locales,
which a printed carrier needs: the passport state a product was placed
on the market with is one moment, not one per language.

Add `publication_releases`: one row per minted version, with a
`sequence` that is monotonic per publication, minted inside the same
lock as the version number. `publication_versions.release_id` points at
it. Existing versions are backfilled, one release each in publish order.

`PublicationRelease::versionsAsOf()` resolves the state as of a release:
for every locale, the most recent version minted at or before it. That
is the read a per-release public route needs and nothing else; this
change alters no public behaviour.

Releases are immutable like versions (update/delete throw), and
`release_id` is sealed on the version.
@midego1

midego1 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Context, motivation and suggested review order for this and the related PRs: #683

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.

1 participant