Merge pull request 'ADR 0257, issues 321-323, research 035: record the bases builds copy; what the pre-mesh images are' (#207) from decision/a-copied-base-is-recorded into main
This commit was merged in pull request #207.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
---
|
||||
status: active
|
||||
initiated: 2026-10-08
|
||||
touches:
|
||||
- 02-DECISIONS/0189-the-store-keeps-what-the-records-name.md
|
||||
- 02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md
|
||||
- 03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md
|
||||
- the artifact store, the catalogue's modules that pin an image by digest
|
||||
---
|
||||
|
||||
# 035 — The images from before the mesh
|
||||
|
||||
## What is investigated
|
||||
|
||||
The artifact store's tools ([ADR 0251](../../02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md))
|
||||
counted 469 unrecorded manifests marking 23.8 GiB on 2026-10-08. 374 of them are copied bases
|
||||
([issue 321](../../04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md),
|
||||
[ADR 0257](../../02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md)).
|
||||
Most of the rest are images pushed to the same store before the mesh existed. The predecessor's
|
||||
pipeline built them, under a namespace per owner, one repository per app.
|
||||
|
||||
No record of the mesh names these images, so [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md)
|
||||
§3 keeps every tool away from them. That is correct: the mesh did not make them. It also means they stay
|
||||
until a person decides. This effort finds out, for each one, whether anything still uses it, and
|
||||
recommends what to do. Nothing was deleted.
|
||||
|
||||
## How it was measured
|
||||
|
||||
All of this was read, nothing was written. On 2026-10-08:
|
||||
|
||||
- **The store's view.** `artifact_store_references` and `artifact_store_repositories` give each
|
||||
repository's manifests, tags and sizes. They give no dates.
|
||||
- **What each machine declares.** The controller's `images` verb answers for each of the four machines.
|
||||
- **What each machine runs.** The container runtime module's container list answers on each machine.
|
||||
- **What still names them.** I searched every repository on the forge for each image's path and digest.
|
||||
There are 50 repositories, and I read 31 of them closely: both catalogues, the apps' own repositories,
|
||||
the setup repositories and the predecessor's.
|
||||
- **Dates.** No tool gives a manifest's push time. A date below is either an image's `created` date, from
|
||||
a copy still in a machine's local image store and matched by tag, or the date of the commit the tag
|
||||
names. Where neither exists, the date is not known, and the table says so.
|
||||
|
||||
## What was found
|
||||
|
||||
[01-the-images.md](01-the-images.md) has the table. In summary:
|
||||
|
||||
- **3 repositories hold an image a machine runs today.** Each is one of three app images on the anchor:
|
||||
an invoicing app's API, its web front, and a static company website. A catalogue module pins each by
|
||||
digest, under an older name of the same store. Each repository also holds two older tags that
|
||||
nothing uses.
|
||||
- **2 repositories are pinned only by catalogue modules that no machine is assigned.** One is a
|
||||
magazine site's older image. The other is an older photo client, pinned by two per-person photo
|
||||
modules. Both are stale definitions.
|
||||
- **35 repositories, plus 2 single manifests in repositories the mesh builds, are used by nothing.**
|
||||
Nothing runs them, declares them or pins them.
|
||||
- Most were superseded by a module that builds its own image of the same app.
|
||||
- The rest are abandoned: a trading bot, a chatbot, test images, the predecessor's dashboard and flow
|
||||
engine, and the bootstrap's first builder and controller images.
|
||||
- Together they mark between 5.9 and 7.5 GiB that nothing else marks. The two bounds depend on how the
|
||||
blobs they share among themselves are counted.
|
||||
|
||||
**A finding beside the question.** The store's tools classify the three running images as
|
||||
*unrecorded*, the same word they use for an image nothing uses. The reason is that the catalogue pins
|
||||
them under an older name of the store, and the mesh did not make them. Nothing removes an unrecorded
|
||||
manifest today, so nothing is at risk. But any later decision to let unrecorded images go by some
|
||||
other rule would take running images with it. That is recorded as
|
||||
[issue 323](../../04-ISSUES/323-an-image-a-definition-pins-under-an-older-name-of-the-store-reads-as-unrecorded/00-report.md).
|
||||
|
||||
## What is recommended
|
||||
|
||||
- **Keep** the three running images. Point their modules at an image the mesh builds, where one exists,
|
||||
so the running images become the mesh's own. Two of the three apps already have a build of their own
|
||||
on a branch.
|
||||
- **Delete the two stale catalogue modules**, then treat their images like the rest.
|
||||
- **Let go of the 35 repositories and 2 manifests** once a decision says how an image the mesh did not
|
||||
make may be removed. ADR 0189 §3 forbids every tool from doing it today, and that is intended. The
|
||||
decision is the operator's: the images are the operator's, and some can no longer be rebuilt.
|
||||
- **Adopt none of them as a module.** Every app in use already has a module.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Should an image the mesh did not make be removed through the controller's records, by a person
|
||||
recording it (as ADR 0257 lets a person record a copied base), or through a separate verb of the
|
||||
store's tools?
|
||||
- Should the older tags in a repository that is in use (about 0.8 GB together) go before their modules
|
||||
are moved to the mesh's own build, or with it?
|
||||
@@ -0,0 +1,64 @@
|
||||
# 035 — The images, one by one
|
||||
|
||||
Read on 2026-10-08. Sizes are the bytes a repository's manifests mark. *Only theirs* is what no other
|
||||
manifest in the store marks, which is what removing the repository would free. A machine is named by its
|
||||
role.
|
||||
|
||||
Dates: *created* is an image's build date, from a copy in a machine's local image store matched by tag;
|
||||
*commit* is the date of the commit the tag names; *not known* means neither was found.
|
||||
|
||||
## In use
|
||||
|
||||
| what | size | manifests | newest | in use by | named by | recommendation |
|
||||
|---|---|---|---|---|---|---|
|
||||
| an invoicing app's API | 1.41 GB | 3 tags | created 2023-01 | the anchor, its API container (newest tag, matched by digest) | its catalogue module, pinned under an older name of the store; also the frozen runtime base of the app's own build on a branch | keep the newest; let the two older go (about 0.7 GB); move the module to the mesh's own build |
|
||||
| the same app's web front | 60.5 MB | 3 tags | created 2023-01 | the anchor, its web container (matched by digest) | its catalogue module, likewise | keep the newest; let the two older go |
|
||||
| a static company website | 474 MB | 3 tags | created 2022-08 | the anchor, the website container (matched by digest) | its catalogue module, likewise; the site's repository has its own build on a branch, not yet delivered | keep the newest; let the two older go (about 75 MB) |
|
||||
|
||||
## Pinned only by a stale module
|
||||
|
||||
| what | size | manifests | newest | in use by | named by | recommendation |
|
||||
|---|---|---|---|---|---|---|
|
||||
| a magazine site, its older image | 393 MB | 3 tags | created 2025-10 | nothing: the anchor runs the mesh-built image | a catalogue module whose definition no machine uses | let the two older go; the newest once the stale module is deleted |
|
||||
| a photo app's older client | 11 MB | 1 | created 2024-10 | nothing: the anchor runs the mesh-built client | two per-person photo modules, assigned to no machine | delete the two modules, then let it go |
|
||||
|
||||
## Used by nothing
|
||||
|
||||
Each row is one repository, unless it says otherwise.
|
||||
|
||||
| what | size (only theirs) | manifests | newest | superseded by, or why abandoned | recommendation |
|
||||
|---|---|---|---|---|---|
|
||||
| a car-listing scraper | 2.31 GB (1.43 GB) | 3 | commit 2026-02 | the module's own build, which the home server runs | let go |
|
||||
| a chatbot's API | 3.04 GB | 1 | before 2024-09 | abandoned since 2024 | let go |
|
||||
| a chatbot's web front | 19.8 MB | 1 | before 2024-09 | abandoned | let go |
|
||||
| a comic app's API and web front (2 repositories) | 828 MB | 1 each | created 2026-09 | the predecessor's; no module | let go, unless the app is wanted back (then build it as a module) |
|
||||
| a photo app's earlier API and web front (2 repositories) | 519 MB | 4 each | not known | the photo modules | let go |
|
||||
| a photo app's server and admin client (2 repositories) | 445 MB | 1 each | created 2024-10 | the photo modules' own builds | let go |
|
||||
| a map game's API and web front (2 repositories) | 121 MB | 3 each | commit 2026-02 | renamed to the travel game below | let go |
|
||||
| a travel game's API and web front (2 repositories) | 104 MB | 2 each | commit 2026-02 | no containers or data remain | let go (it can be rebuilt from its repository) |
|
||||
| a racing-circuit site | 264 MB | 3 | created 2026-06 | the module's own build, which the home server runs | let go |
|
||||
| an older company website | 400 MB | 1 | not known | the static website above, and its own build | let go |
|
||||
| a payment-link site | 52.6 MB | 3 | before 2025-11 | the module's own build | let go |
|
||||
| a mail-forwarding service | 58.5 MB | 1 | created 2026-03 | its repository's own module, assigned nowhere | let go |
|
||||
| a snake game and a text game (2 repositories) | 88 MB (0.8 MB) | 1 each | created 2026-08 and 2026-09 | the modules' own builds | let go |
|
||||
| a trading bot | 85 MB | 3 | commit 2022-08 | abandoned | let go |
|
||||
| a trading bot's scheduled jobs and web front (2 repositories) | 83 MB | 1 each | not known | abandoned (setup last changed 2025-01) | let go |
|
||||
| three base images of a former owner namespace (3 repositories) | 188 MB | 1 each | not known | abandoned (setup last changed 2023-05) | let go |
|
||||
| test images (3 repositories) and a capability test | 135 MB | 1 to 3 | not known | tests of the predecessor | let go |
|
||||
| the predecessor's dashboard and flow-engine API (2 repositories) | 165 MB (157 MB) | 2 and 3 | created 2026-09 | the predecessor is retired | let go |
|
||||
| the bootstrap's first builder and controller images (2 repositories) | 30 MB (26 MB) | 1 each | created 2026-09 | the build agent; the controller runs as a bundle | let go |
|
||||
| a build agent's and the controller's former server images (2 repositories) | 62 MB (55 MB) | 2 each | created 2026-09 | the same | let go |
|
||||
| one manifest in the mesh-built tools image's repository | 87 MB (6.6 MB) | 1 of 4 | not known | the three recorded beside it | let go |
|
||||
| one manifest in the route proxy's server repository | 9.9 MB (6.0 MB) | 1 of 6 | not known | the kept build the machines declare | let go |
|
||||
|
||||
**Together**, the rows used by nothing mark between 5.9 GiB and 7.5 GiB that nothing outside them marks.
|
||||
The upper bound sums each repository's own bytes. That counts twice a blob two of them share. The lower
|
||||
bound subtracts every shared blob, and that also subtracts blobs shared only among these rows, which
|
||||
removing all of them would free. Removing the two stale modules' images adds 0.4 GiB. Removing the older
|
||||
tags of the three images in use adds about 0.8 GB.
|
||||
|
||||
**Not determined.**
|
||||
|
||||
- A push date for 14 of the repositories.
|
||||
- Whether the forge listing of 50 repositories is all of them. A second page came back empty, so I think
|
||||
it is.
|
||||
@@ -36,6 +36,13 @@ image the manifest did not declare is refused before the build, naming the image
|
||||
its own stages, declared arguments and `scratch` are not fetches. An unpinned vendor image is
|
||||
refused: a tag is what somebody else can move.
|
||||
|
||||
> **The mechanism changed — 2026-10-08, by [ADR 0257](0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md).**
|
||||
> A declared image is still copied into the mesh's registry before the build, and the recipe is still handed
|
||||
> the copy. What moved is where the copy goes. It no longer goes "under the module's repository"
|
||||
> (`<module>/on-<argument>`). It goes into one repository per upstream image, `upstream/<host>/<path>`,
|
||||
> whichever modules stand on it. Each copy is also recorded where it is made, so the store's sweep can
|
||||
> let it go.
|
||||
|
||||
A recipe whose `FROM` names an undeclared base was at first said, not refused: the mesh's own
|
||||
images — the control plane's, the tool runtime's, the route proxy's — started from a public base
|
||||
and declared none, and refusing those refuses genesis. *Amended the same day:* those three declare
|
||||
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-08
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
supersedes-in-part:
|
||||
- 0097-a-vendor-image-is-a-declared-build-input.md
|
||||
extends: 02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md
|
||||
---
|
||||
|
||||
# 257. A copied base is recorded where it is copied, kept by the builds that stood on it, and held once per image
|
||||
|
||||
## Context
|
||||
|
||||
A module's build may stand on an image published elsewhere, declared in its manifest and pinned by digest
|
||||
([ADR 0097](0097-a-vendor-image-is-a-declared-build-input.md)). Before the build, the builder copies that
|
||||
image into the artifact store, whole, between registries
|
||||
([ADR 0096](0096-an-upstream-image-is-copied-between-registries.md)), under the module's own repository,
|
||||
`<module>/on-<argument>`. This record calls such a copy a **copied base**.
|
||||
|
||||
[ADR 0251](0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md)
|
||||
gave the store tools that classify every manifest against the controller's records. Their first reading,
|
||||
on 2026-10-08, found 469 manifests marking 23.8 GiB that no record names, so nothing would ever remove
|
||||
them ([ADR 0189](0189-the-store-keeps-what-the-records-name.md) §3). Issue
|
||||
[321](../04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md)
|
||||
has the details. 374 of those manifests, in 27 repositories, are copied bases. Four facts explain them:
|
||||
|
||||
1. **The copy is recorded as something the build stood on, never as something it made.** The records
|
||||
name what each build *made*, and the sweep reads only that. A build's `built_against` does name the
|
||||
copy, but the sweep does not read it.
|
||||
2. **A build that fails after copying records the copy nowhere.** A failed build keeps no module and no
|
||||
`built_against`, yet the copy is in the store from the moment it is made.
|
||||
3. **An upstream image is an index over several platforms.** Each platform manifest is put under its own
|
||||
digest, so each is a manifest of the repository in its own right. The store's collector keeps every
|
||||
manifest a repository holds. Letting the index go therefore frees none of the bytes of the images it
|
||||
names. This is true of every upstream artifact the mesh has ever let go of, not only of copied bases.
|
||||
4. **There is one copy per module, not one per image.** Several modules standing on one image hold
|
||||
several copies. Their bytes are stored once, because the store keeps each blob once whichever
|
||||
repositories link it, so the copies cost little disk. But each module's first copy asks the public
|
||||
registry for the whole image again, and that registry rations anonymous pulls.
|
||||
|
||||
The largest single case is a route proxy's Go toolchain image: 34 manifests and 10.4 GiB in one
|
||||
repository. That is two versions of the toolchain image, each with every platform it is published for,
|
||||
Windows included.
|
||||
|
||||
## Considered Options
|
||||
|
||||
**What records a copy.**
|
||||
|
||||
1. *Read `built_against` as the record.* Rejected as the only record, because it misses fact 2: a
|
||||
failed build has none. It is kept as the source of the backfill (point 2 below).
|
||||
2. **A record of its own, written when the build reports, whether the build worked or not.** Chosen.
|
||||
The builder says what it copied in every outcome it announces. The controller keeps one row per copy.
|
||||
|
||||
**What keeps a copy.**
|
||||
|
||||
1. *A definition naming it, as for an artifact.* Rejected. A definition names the image upstream, by the
|
||||
digest that every copy of that image shares. As a reason it would keep every module's copy of an image
|
||||
for as long as any module stood on it. And no machine is ever handed a base, so a definition is never
|
||||
the reason a base is needed.
|
||||
2. *Its own five most recent copies.* Rejected: a copy has no builds of its own to count.
|
||||
3. **The kept builds that stood on it.** Chosen. A copy is kept while one of the five most recent
|
||||
successful builds of a module the mesh holds stood on it. That is the build ADR 0189 already keeps for
|
||||
a release to go back to, and it is the one a rebuild would need the copy for.
|
||||
|
||||
**Where a copy lives.**
|
||||
|
||||
1. *Under the module, as now.* Rejected for fact 4.
|
||||
2. **One repository per upstream image, named for the image: `upstream/<host>/<path>`.** Chosen. The
|
||||
first module to stand on an image copies it, and every later module finds it already held. Two images
|
||||
with one path on different hosts are not confused. On the move, a copy the module's own repository
|
||||
already holds is taken from there: its manifests are read from the store and its blobs are mounted
|
||||
across. Upstream is not asked.
|
||||
|
||||
**What becomes of an index's platforms.**
|
||||
|
||||
1. *Let the index go alone.* Rejected for fact 3.
|
||||
2. *Let the index go with every platform manifest it names.* Rejected, because a kept index of the same
|
||||
repository may name one of them.
|
||||
3. *The same, in every sweep.* Rejected. The sweep after each build is real collection with no dry run
|
||||
in front of it. The controller reads only its records, so it cannot see an unrecorded index, or a copy
|
||||
a build is making at that moment, that names the same platform.
|
||||
4. **Let the index go with the platform manifests it names that no kept index of the repository also
|
||||
names, platforms first, only in a collect a person confirmed.** Chosen. The store tools can see every
|
||||
manifest in a repository, and their dry run lists each one that stays and would lose a platform. A
|
||||
person reads that before confirming. The sweep after a build does not touch an index: it leaves it
|
||||
eligible for that collect. Letting it go alone would be option 1, and would record it as collected,
|
||||
so no later collect would offer it again and its platforms would stay for ever.
|
||||
|
||||
**The copies no record names**, made before this record by a failed build, or before builds kept what
|
||||
they stood on.
|
||||
|
||||
1. *Remove every manifest in a copy's repository by name.* Rejected: ADR 0189 §3 keeps removal to what a
|
||||
record names, and a repository's name is not a record.
|
||||
2. *Leave them for ever.* Rejected: they are most of what the store carries for no reason.
|
||||
3. **A person records them, and the sweep then decides as for every other copy.** Chosen. The store
|
||||
tools list each unrecorded copy at the top of a copy's repository, with what it alone marks. A new
|
||||
controller verb, `mirrors`, records the ones a person names, with a reason. It refuses any reference
|
||||
outside a repository only the copy step writes, and any reference the store does not hold. Recording
|
||||
deletes nothing itself; a recorded copy then goes at the next sweep unless a kept build stood on it.
|
||||
|
||||
**Copying only the platforms the mesh runs** was weighed and is left for a later record. It would remove
|
||||
most of the route proxy's toolchain copy, which is Windows images, but the reference a build is handed
|
||||
would then no longer be the upstream digest. Whether the store accepts an index whose platforms are
|
||||
partly absent depends on its version and settings, and that has not been measured.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. One repository per upstream image.** A copied base is put in `upstream/<host>/<path>` (the public
|
||||
hub is `docker.io`, and a port's colon becomes a dash). The module's own repository from before is
|
||||
offered as a source: where it holds the image by digest, the copy is made from it, and nothing is asked
|
||||
of upstream. A copy counts as held only when its index and every platform manifest the index names are
|
||||
held; otherwise it is copied again, which puts back what is missing. A module's build arguments name the
|
||||
new copy. Its source fingerprint is unchanged, because
|
||||
the fingerprint reads a base by digest, never by where it was copied to.
|
||||
|
||||
**2. A copy is recorded where it is copied.** The builder says, in every build outcome, what it copied.
|
||||
It says so when the build fails too. The controller keeps one row per copy in `artifact_mirrored`: the
|
||||
build that first said it, or a person's reason. A migration records every copy that the existing build
|
||||
records already name in a module's `<module>/on-<argument>` repository, each by the earliest build that
|
||||
named it. Only the copy step ever wrote a repository so named.
|
||||
|
||||
**3. A copy is kept by the kept builds that stood on it, and by nothing else.** A copy is kept, with the
|
||||
reason `stood-on`, while one of the five most recent successful builds of a module the mesh holds stood
|
||||
on it. Otherwise it is eligible, and the sweep lets it go as it lets any eligible artifact go. A
|
||||
definition never keeps a copy. A copy already let go of stays let go of, until a build copies it again:
|
||||
the build then holds it, and its record of being let go of is removed. The reason `stood-on` belongs to
|
||||
copies alone: another module's artifact is still kept by that module's own builds.
|
||||
|
||||
**4. An index goes with its platforms only in a collect a person confirmed.** Before its first delete,
|
||||
a confirmed `collect` reads, for every repository it will touch, which platform manifests the kept
|
||||
indexes there name. If any of those reads fails, it lets nothing go. Then, for each index it lets go of,
|
||||
it first deletes the platform manifests that no kept index of the same repository names, and deletes the
|
||||
index last. A collect cut short leaves the index to be asked about again. The sweep after a build asks
|
||||
the store whether each eligible reference is an index (one read of its manifest). It skips every index,
|
||||
leaving it eligible and not recorded as collected, and lets go of everything else as before; a read that
|
||||
fails stops the sweep. This applies to every index the mesh lets go of, upstream artifacts included.
|
||||
|
||||
**5. A person may record a copy that no record names.** The controller's `mirrors` verb lists the copies
|
||||
the records hold, with their states. Given references, it records the ones that are in a repository only
|
||||
the copy step writes and that the store holds. It is a dry run without `confirm`. With `confirm` it needs
|
||||
a `why`, and it is recorded as a hand act once the copies are recorded. It deletes nothing itself, but a
|
||||
recorded copy is eligible, and the next sweep lets it go unless a kept build stood on it; the dry run says
|
||||
so before anything is recorded. `artifact_store_references` lists the
|
||||
candidates in `unrecorded_mirrors`, largest first.
|
||||
|
||||
**6. The store tools count a platform as its index's.** A platform manifest takes the state of the
|
||||
recorded index of its repository that names it, the more keeping state when two do. An index no record
|
||||
names leaves its platforms unrecorded with it. `artifact_store_collect` counts the platforms that go with
|
||||
an index, and its dry run lists, in `platforms_named_by_what_stays`, every manifest that stays and names a
|
||||
platform that would go.
|
||||
|
||||
ADR 0097 is superseded in one sentence only: a base is copied into the image's own repository, no longer
|
||||
"under the module's repository". Everything else ADR 0097 decided stands.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The records answer for every copy a build makes from now on, and the sweep keeps or lets go of copies
|
||||
by the same rule as the artifacts.
|
||||
- **Recording the copies frees little by itself.** Measured on 2026-10-08 against the records:
|
||||
- 17 of the 23 recorded copies were stood on by a kept build.
|
||||
- The 6 that were not hold about 0.4 GiB that only they mark.
|
||||
- Most copies share their bytes with a kept copy of the same image, so letting them go frees their
|
||||
manifests, not their bytes.
|
||||
- Most of the 23.8 GiB is copies no record names, and pre-mesh images (research
|
||||
[035](../01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md)). Those wait for a person,
|
||||
through `mirrors` and, for the pre-mesh images, a decision of its own.
|
||||
- **Rolling the controller back is not free once old copies are gone.** After this is rolled out, the
|
||||
modules' former `<module>/on-<argument>` copies stop being stood on and are let go of. A controller
|
||||
rolled back to the version before this record names those repositories again. It finds them empty and
|
||||
copies every base from the public registry again, which rations anonymous pulls. A rollback after that
|
||||
point should be planned around the pull limit.
|
||||
- The move to one repository per image costs one copy per image, taken from the store itself. After it,
|
||||
each module's former copies stop being stood on, become eligible after five more builds of that
|
||||
module, and go, freeing their manifests.
|
||||
- A module named `upstream` would collide with the copies' namespace. The build edges already ignore
|
||||
the copies' repositories, so no edge is drawn to such a module. Refusing the name is left to the
|
||||
catalogue's module check, and it is not done here.
|
||||
- The kept copy of the route proxy's toolchain still holds its Windows platforms. I estimate them at
|
||||
about 4.9 GB, from the two platform manifests of each version that are larger than 2 GB; which index
|
||||
names which platform was not read. Removing them is for the later record on copying only the platforms
|
||||
the mesh runs.
|
||||
|
||||
## How it is checked
|
||||
|
||||
- **The builder.**
|
||||
- The repository is named for the image: the hub, other hosts, a port, upper case.
|
||||
- A copy held under the module's former repository is mounted, every blob, with nothing uploaded
|
||||
again and nothing asked of an upstream that is gone. A store that answers a mount with an upload
|
||||
gets each blob uploaded.
|
||||
- An index missing a platform, in the image's repository or the former one, is not taken as held.
|
||||
- Builds copying while platforms are let go of under them end with the copy whole.
|
||||
- A second module standing on the image copies nothing.
|
||||
- A failed build and a working build both say what they copied.
|
||||
- **The records.**
|
||||
- A copy only a failed build made is eligible.
|
||||
- A copy is kept, with `stood-on`, by the five kept builds and by nothing else, and a definition
|
||||
naming the image keeps no copy.
|
||||
- Another module's artifact is not kept as a copy.
|
||||
- A collected copy stays collected, and a copy a build copies again is held again; a person's record
|
||||
never brings a collected copy back.
|
||||
- A person's record stands over a later build's.
|
||||
- The migration's backfill records one copy, by its earliest build, and records nothing outside an
|
||||
`on-` repository.
|
||||
- A copy's repository draws no build edge.
|
||||
- **The sweep.**
|
||||
- Through the records, a confirmed collect lets an eligible index go with its own platform, platforms
|
||||
first, and leaves the platform a kept index of the same repository names.
|
||||
- The sweep after a build leaves an eligible index eligible and lets an image go, and a later confirmed
|
||||
collect takes the index with its own platform.
|
||||
- An image is one delete.
|
||||
- A kept index the store will not answer for stops a confirmed collect before its first delete, and an
|
||||
index in a repository not read beforehand is kept.
|
||||
- **The `mirrors` verb.**
|
||||
- It is composed only with what it takes.
|
||||
- It takes only references in a copy's repository.
|
||||
- A dry run records nothing.
|
||||
- A real run records what the store holds, and the sweep then offers it.
|
||||
- It never asks the store anything but HEAD.
|
||||
- **The store tools.** Over a view with two indexes of one image and an unrecorded copy:
|
||||
- A platform takes its index's state, the more keeping state when two indexes name it.
|
||||
- The unrecorded copy's top is offered with what it alone marks.
|
||||
- Letting the old index go takes only its own platform.
|
||||
- Sparing nothing would be said as a kept index losing a platform.
|
||||
- **Live.** After rollout, `mesh-controller.collect` (dry run), `mesh-controller.mirrors` and
|
||||
`artifact_store_references` are read. Their numbers are recorded in to-be 51.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0096 — an upstream image is copied between registries](0096-an-upstream-image-is-copied-between-registries.md)
|
||||
- [ADR 0097 — a vendor image is a declared build input](0097-a-vendor-image-is-a-declared-build-input.md)
|
||||
- [ADR 0189 — the store keeps what the records name](0189-the-store-keeps-what-the-records-name.md)
|
||||
- [ADR 0251 — the registries say what they hold and keep what is named](0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md)
|
||||
- [issue 321 — a copied base was recorded by nothing, and a let-go index kept its platforms](../04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md)
|
||||
- [research 035 — the images from before the mesh](../01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md)
|
||||
- [to-be 51 — the registries keep what is named](../03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md)
|
||||
@@ -2,7 +2,7 @@
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code:
|
||||
- mesh-controller cmd/mesh-controller (the verbs artifacts, collect and images)
|
||||
- mesh-controller cmd/mesh-controller (the verbs artifacts, collect, images and mirrors), internal/builder (copied bases), internal/inventory (artifact_mirrored)
|
||||
- mesh-catalog modules/artifact-store-tools (the store's tools), modules/distribution (its unbuilt code removed)
|
||||
- mesh-catalog modules/gitea (package retention)
|
||||
- mesh-catalog modules/docker (image pruning)
|
||||
@@ -10,6 +10,7 @@ updated: 2026-10-08
|
||||
decisions:
|
||||
- 02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md
|
||||
- 02-DECISIONS/0189-the-store-keeps-what-the-records-name.md
|
||||
- 02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md
|
||||
---
|
||||
|
||||
# 51 — The registries keep what is named
|
||||
@@ -80,6 +81,7 @@ the same mark, so their numbers are the collector's.
|
||||
|---|---|---|
|
||||
| kept — definition | a module's recorded definition names it | no |
|
||||
| kept — recent build | one of the five most recent builds of a module the mesh holds | no |
|
||||
| kept — stood on | a copied base one of those builds stood on (ADR 0257) | no |
|
||||
| holder of a kept archive | the manifest that keeps a kept archive's blob | no |
|
||||
| eligible | the mesh made it and keeps it for no reason | yes, through the controller |
|
||||
| let go of, yet present | the controller recorded letting it go, and the store still holds it | no — said, for a person to look at |
|
||||
@@ -90,7 +92,37 @@ the same mark, so their numbers are the collector's.
|
||||
A holder is recognised by its shape: a manifest with an empty configuration and one layer, which is
|
||||
the blob of an archive the records name.
|
||||
|
||||
### The controller's two verbs for the store
|
||||
**A platform manifest is its index's** ([ADR 0257](../../02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md)).
|
||||
An upstream image is an index over several platforms, and each platform manifest is a manifest of the
|
||||
repository in its own right. A platform manifest takes the state of the recorded index of its
|
||||
repository that names it, and the more keeping state when two indexes name it. An index that no record
|
||||
names leaves its platforms unrecorded with it.
|
||||
|
||||
### Copied bases
|
||||
|
||||
A build that stands on an image published elsewhere copies it into the store first. Since
|
||||
[ADR 0257](../../02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md):
|
||||
|
||||
- **One repository per upstream image**, `upstream/<host>/<path>`, whichever modules stand on it. A copy
|
||||
that a module's former repository, `<module>/on-<argument>`, already holds is copied from there, with
|
||||
each blob mounted across, and upstream is not asked.
|
||||
- **Recorded where it is copied.** The builder says what it copied in every build outcome, including a
|
||||
failed one, and the controller keeps a row per copy in `artifact_mirrored`. The copies the existing
|
||||
build records already named were recorded by the migration that made the table.
|
||||
- **Kept by the kept builds that stood on it** (`stood-on`), and by nothing else: a definition never
|
||||
keeps a copy. Otherwise a copy is eligible, like any artifact the mesh made.
|
||||
- **Held whole.** A copy counts as held only when its index and every platform the index names are
|
||||
held; otherwise it is copied again. A copy a build copies again is no longer recorded as let go of.
|
||||
- **An index goes with its platforms only in a confirmed collect.** Before its first delete, it reads
|
||||
which platforms the kept indexes of every repository it touches name, and lets nothing go if a read
|
||||
fails. It deletes the platform manifests an index names that no kept index of its repository also
|
||||
names, and deletes the index last. The sweep after a build skips every eligible index and leaves it
|
||||
eligible for such a collect; it lets other eligible artifacts go as before.
|
||||
- **A copy no record names** waits for a person. `artifact_store_references` lists each one in
|
||||
`unrecorded_mirrors`, with what it alone marks. The controller's `mirrors` verb records the ones a
|
||||
person names, and the sweep then decides about them.
|
||||
|
||||
### The controller's verbs for the store
|
||||
|
||||
- **`artifacts`** reads only. It answers every reference the mesh recorded making, with its state and,
|
||||
for kept ones, the reason; collected ones only when `collected` is asked for; narrowed to one
|
||||
@@ -98,7 +130,17 @@ the blob of an archive the records name.
|
||||
- **`collect`** says what the sweep would let go of. With `confirm` and a `why` it runs the sweep that
|
||||
runs after a build: every kept archive held first, then each eligible reference let go and recorded
|
||||
as collected, stopping at the first refusal by the store. It is bounded by a count and a time, says
|
||||
what is left, and is a hand-act recorded with its `why`.
|
||||
what is left, and is a hand-act recorded with its `why`. A confirmed collect lets an index go with its
|
||||
own platform manifests (ADR 0257); the sweep after a build leaves an eligible index for it.
|
||||
- **`mirrors`** lists the copied bases the records hold, with their states. Given references, it records
|
||||
the copies that are in a repository only the copy step writes and that the store holds. It is a dry
|
||||
run without `confirm`; with `confirm` it needs a `why`, and it is a hand act, logged once the copies
|
||||
are recorded. It deletes nothing itself, but a recorded copy is eligible: the next sweep lets it go
|
||||
unless a kept build stood on it, and both the dry run and the real run say so.
|
||||
|
||||
`artifact_store_collect` counts the platforms that go with an index, and its dry run lists, in
|
||||
`platforms_named_by_what_stays`, every manifest that stays and names a platform that would go. The
|
||||
controller cannot see such a manifest, because it reads only its records.
|
||||
|
||||
## The package registry
|
||||
|
||||
@@ -118,6 +160,13 @@ repository on the forge.
|
||||
Ranges are read as npm reads them: exact, `^`, `~`, `x` and `*`, comparisons, hyphen ranges and `||`.
|
||||
A range that cannot be read is named, and its package keeps every version until it can.
|
||||
|
||||
Lockfiles are read in all three of their versions: version 1's nested `dependencies`, and the `packages`
|
||||
of versions 2 and 3, where an entry's own `dependencies` is a map of names to ranges. A file that could
|
||||
not be read whole is named, and like an unread repository it stops a real run before anything is deleted
|
||||
([issue 322](../../04-ISSUES/322-package-retention-read-no-lockfile-and-said-zero-bytes-for-every-kept-version/00-report.md)).
|
||||
Every version is measured from the forge's file sizes, and a size that could not be read is said, never
|
||||
counted as zero.
|
||||
|
||||
## A machine's images
|
||||
|
||||
The container runtime module's tool `docker_prune_images` works on the machine it runs on.
|
||||
@@ -150,3 +199,12 @@ removed and what the runtime refused.
|
||||
|
||||
*In progress, 2026-10-08.* The decision is recorded; the code is being built in the controller and in
|
||||
the store, forge and container runtime modules. The live dry runs are recorded here once the tools run.
|
||||
|
||||
The first readings, 2026-10-08, before ADR 0257 was rolled out:
|
||||
|
||||
- **The store** held 25.7 GiB. 469 manifests marking 23.8 GiB were unrecorded. Of those, 374 manifests in
|
||||
27 repositories were copied bases ([issue 321](../../04-ISSUES/321-a-copied-base-was-recorded-by-nothing-and-a-let-go-index-kept-its-platforms/00-report.md)),
|
||||
and most of the rest were images pushed before the mesh existed
|
||||
([research 035](../../01-RESEARCH/035-the-images-from-before-the-mesh/00-overview.md)).
|
||||
- **The package registry** held one package with four versions, all kept. Every lockfile on the forge
|
||||
went unread, and every version read zero bytes ([issue 322](../../04-ISSUES/322-package-retention-read-no-lockfile-and-said-zero-bytes-for-every-kept-version/00-report.md)).
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
---
|
||||
status: located
|
||||
opened: 2026-10-08
|
||||
located-in: [mesh-controller internal/builder (standingOn, the copy of a base), internal/inventory (the keep set), internal/artifacts (LetGo), cmd/mesh-controller (the sweep); mesh-catalog modules/artifact-store-tools (classification)]
|
||||
fixed-by:
|
||||
amended-design: 03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md
|
||||
---
|
||||
|
||||
# 321. A copied base was recorded by nothing, and a let-go index kept its platforms
|
||||
|
||||
## Symptom
|
||||
|
||||
On 2026-10-08 at 13:35, `artifact_store_references` (the store's tools of
|
||||
[ADR 0251](../../02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md))
|
||||
read the artifact store at 25.7 GiB. 469 manifests marking 23.8 GiB were **unrecorded**: no controller
|
||||
record names them, so no sweep will ever let them go
|
||||
([ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md) §3).
|
||||
|
||||
374 of those manifests are in 27 repositories named `<module>/on-<argument>`. Only the builder writes
|
||||
such a name: before a build, it copies there the image published elsewhere that the module's build
|
||||
stands on ([ADR 0097](../../02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md)). The largest
|
||||
is a route proxy's Go toolchain copy: 34 manifests, 10.4 GiB, all unrecorded.
|
||||
|
||||
The same reading shows a second case. The repository of the route proxy's upstream artifact (a mirrored
|
||||
`upstream` image, kind `upstream`) has one kept manifest of 23 KiB and 16 unrecorded manifests marking
|
||||
30 MB.
|
||||
|
||||
## Why it is a design issue
|
||||
|
||||
ADR 0189 rests on one sentence: the mesh "has never put anything there it did not record". For copied
|
||||
bases that sentence is false, and nothing said so until the store's tools counted it. Each copy is a
|
||||
thing the mesh put in its store, and the records do not show it as made. So the sweep can never let a
|
||||
copy go, however old it is, and the store only grows. The second case is the same promise broken
|
||||
another way: letting an index go was recorded as collecting it, and the store kept every byte it named.
|
||||
|
||||
The diagnosis is in [01-diagnosis.md](01-diagnosis.md).
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
# 321 — Diagnosis
|
||||
|
||||
## 2026-10-08
|
||||
|
||||
**Where a copy is made.** The builder resolves a module's declared bases before it builds anything.
|
||||
For an image published elsewhere, it copies the image into the store under
|
||||
`<module>/on-<lower-cased argument>` and hands the recipe the copy. It also returns the copy as one of
|
||||
the references the build stood on. The controller keeps those references in the build's
|
||||
`built_against`. It keeps the build's artifacts in `made`.
|
||||
|
||||
**What the sweep reads.** The keep set and the list of everything the mesh made are both read from
|
||||
`made`. `built_against` is read only for build edges. So a copy is in the records, but not where the
|
||||
sweep looks, and it is never counted as something the mesh made.
|
||||
|
||||
**A failed build.** The controller records a failed build with no module and no `built_against`. A
|
||||
build that copied its base and then failed, for example on a recipe refused for an undeclared fetch,
|
||||
leaves its copy in the store with no record at all. Reading the records on 2026-10-08:
|
||||
|
||||
- 23 distinct copies are named in `built_against` of successful builds.
|
||||
- 17 of them were stood on by one of the five most recent builds of a module the controller holds.
|
||||
- Several `on-` repositories in the store are named by no build at all. Examples: three copies of an
|
||||
invoicing app's bases, from builds whose rows have no `built_against`; a module's Node base; and a
|
||||
second version of the route proxy's toolchain image.
|
||||
|
||||
**Why one per module.** The repository name is built from the module's name, so two modules standing
|
||||
on one image hold two copies. The store keeps each blob once, so the copies cost manifests rather than
|
||||
bytes. But the first copy into each repository asks the public registry for the whole image again. The
|
||||
public registry rations anonymous pulls, and on 2026-09-28 it refused a whole catalogue's rebuild.
|
||||
|
||||
**Why 10.4 GiB.** A copy is an image index with every platform the upstream publishes, Windows
|
||||
included (ADR 0096 copies the index whole). The route proxy's repository holds two versions of a Go
|
||||
toolchain image, each an index with 16 platform manifests. Four of those platform manifests are each
|
||||
larger than 2 GB. I think those four are the Windows images, two per version, because the Linux
|
||||
platforms are around 300 MB each; the index contents were not read to confirm it.
|
||||
|
||||
**Why letting an index go frees nothing.** The copy puts each platform manifest under its own digest,
|
||||
so each is a revision of the repository. The store's collector, run without deleting untagged
|
||||
manifests, keeps every revision a repository holds. The sweep deletes the index's revision and nothing
|
||||
else, so the platform manifests stay, still revisions, and every blob they name stays with them. The
|
||||
store's tools show this in the route proxy's upstream-artifact repository: one kept index, and 16
|
||||
unrecorded platform manifests left behind by the indexes collected before it.
|
||||
|
||||
**Ruled out.**
|
||||
|
||||
- *The store's tools misclassify.* They classify a platform manifest on its own, which is also what the
|
||||
store's collector does. So "unrecorded" is what collection actually sees.
|
||||
- *Blob sharing makes the copies free.* It makes duplicate copies cheap. It does not make old versions
|
||||
of an image free, and nothing ever lets them go.
|
||||
|
||||
**Located** in the controller: the builder (where a copy is made and named), the inventory (what the
|
||||
sweep reads), and the store client (what letting an index go deletes). The store's tools are located
|
||||
there too, because they count a platform manifest apart from its index.
|
||||
|
||||
**What the fix leaves.** The 16 platform manifests in the route proxy's upstream-artifact repository
|
||||
stay. The indexes that named them were let go of before this fix, so no record names them, and neither
|
||||
the sweep nor a confirmed collect will touch them. They can be removed in one of two ways, each a
|
||||
person's act. One is the `mirrors` verb's way of recording them, if the decision is extended to the
|
||||
repository of an upstream artifact. The other is a decision on how the store lets go of manifests no
|
||||
record names (research 035 asks the same question for the pre-mesh images).
|
||||
|
||||
**The fix** is [ADR 0257](../../02-DECISIONS/0257-a-copied-base-is-recorded-where-it-is-copied-kept-by-the-builds-that-stood-on-it-and-held-once-per-image.md)
|
||||
and its two pull requests (the controller and the catalogue's store tools).
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
---
|
||||
status: located
|
||||
opened: 2026-10-08
|
||||
located-in: [mesh-catalog modules/gitea (the npm-registry bundle: refs.go, retention.go)]
|
||||
fixed-by:
|
||||
amended-design: 03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md
|
||||
---
|
||||
|
||||
# 322. Package retention read no lockfile, and said zero bytes for every kept version
|
||||
|
||||
## Symptom
|
||||
|
||||
On 2026-10-08 at 13:45, the forge's `npm_retention` was run as a dry run, with these results:
|
||||
|
||||
- It read 50 repositories.
|
||||
- It found one package with four versions and kept all four.
|
||||
- It listed 32 `package-lock.json` files under `files_not_read`, each with the same error:
|
||||
`json: cannot unmarshal string into Go struct field npmLockEntry.packages.dependencies of type main.npmLockEntry`.
|
||||
- Every version read 0 bytes. The forge's own file sizes for the same versions, read through
|
||||
`npm_packages`, are 20 954 to 28 426 bytes.
|
||||
|
||||
## Why it is a design issue
|
||||
|
||||
[ADR 0251](../../02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md)
|
||||
§4 keeps a version when "a lockfile on the default branch of any repository on the forge names it". No
|
||||
lockfile was read, so that rule kept nothing. Two further faults were silent:
|
||||
|
||||
- **A size that was never measured read as zero.** The dry run asked the forge for the size of a version
|
||||
only when it would delete that version. Every kept version therefore answered 0 bytes, with no word
|
||||
that it had not been measured.
|
||||
- **An unread file did not stop a real run.** Only an unread *repository* did. A real run would have
|
||||
deleted a version that only an unread lockfile named, while its answer listed that file as unread.
|
||||
|
||||
## Diagnosis
|
||||
|
||||
1. **The reader knew one lockfile shape.** A lockfile of version 2 or 3 has a `packages` map from paths
|
||||
to entries. In an entry, `dependencies` (like `devDependencies`, `optionalDependencies` and
|
||||
`peerDependencies`) is a map from names to range strings. The reader decoded every entry with the
|
||||
type of a version 1 entry, whose `dependencies` is a map of nested entries. One string failed the
|
||||
whole file.
|
||||
2. **Sizes were read only for the versions to delete.** This was an optimisation, and it made a kept
|
||||
version's size indistinguishable from an empty one.
|
||||
3. **The refusal checked repositories, not files.**
|
||||
|
||||
**The fix** is in the forge module's Go bundle:
|
||||
|
||||
- It reads versions 1, 2 and 3 one entry at a time. The package name comes from the path after the last
|
||||
`node_modules/`, or from an alias's `name`. Workspace links are skipped.
|
||||
- An entry of an unknown shape is named, and the rest of the file is still read.
|
||||
- Every version is measured, and a size that could not be read is said.
|
||||
- A file not read whole stops a real run, as an unread repository does.
|
||||
|
||||
Tests use real lockfile shapes of all three versions, and a fake forge that serves file sizes.
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-10-08
|
||||
located-in: []
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 323. An image a definition pins under an older name of the store reads as unrecorded
|
||||
|
||||
## Symptom
|
||||
|
||||
On 2026-10-08, `artifact_store_references` classified three manifests as **unrecorded**: "no record names
|
||||
it". The anchor runs all three in containers today. They are an invoicing app's API and web front, and a
|
||||
static company website. Each is pinned by digest in a catalogue module's definition, under an older name
|
||||
of the same artifact store. The anchor's declaration names them, and its containers run them; both were
|
||||
matched by digest ([research 035](../../01-RESEARCH/035-the-images-from-before-the-mesh/01-the-images.md)).
|
||||
|
||||
The same word, *unrecorded*, describes 35 repositories and 2 manifests that nothing runs, declares or pins.
|
||||
|
||||
## Why it is a design issue
|
||||
|
||||
[ADR 0251](../../02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md)
|
||||
§2 lists the states a manifest is in. *Kept — a definition names it* is read only for what the mesh made.
|
||||
An image a definition names, which the mesh did not make, falls through to *unrecorded*. So the answer
|
||||
that should tell a person "a machine runs this" tells them the same as "nothing knows this".
|
||||
|
||||
Nothing removes an unrecorded manifest today (ADR 0189 §3), so no running image is at risk now. The risk
|
||||
comes later. Research 035 asks how the images nothing uses may be let go of. A rule that reads
|
||||
*unrecorded* as *unused* would take three running images with it, and nothing in the answer would warn
|
||||
the person who runs it.
|
||||
|
||||
## Questions to answer
|
||||
|
||||
- Should the store's tools read the definitions, under every name the store has had, and say *named by a
|
||||
definition* for a manifest the mesh did not make?
|
||||
- Or should the three modules be moved to images the mesh builds, so that the case no longer exists, and
|
||||
the catalogue's module check refuse a definition that pins an image under an older name of the store?
|
||||
Reference in New Issue
Block a user