Skip to content

fix(publish): write OCI created into image config so registries order tags#125

Open
mduthey wants to merge 1 commit into
mainfrom
fix/oci-config-created-timestamp
Open

fix(publish): write OCI created into image config so registries order tags#125
mduthey wants to merge 1 commit into
mainfrom
fix/oci-config-created-timestamp

Conversation

@mduthey

@mduthey mduthey commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

fix(publish): write OCI created into the image config so registries order tags

Problem

When publishing two versions of the same protocol (0.1.0, then 0.2.0) to zot, GraphQL search queries always returned the first version published as the newest. For example, this
query returned NewestImage.Tag = "0.1.0" even though 0.2.0 was pushed afterwards:

query GlobalSearch {
  GlobalSearch(requestedPage: { limit: 10, offset: 0, sortBy: ALPHABETIC_DSC } query: "") {
    Repos { Name NewestImage { Tag } LastUpdated }
  }
}

The telling detail: in the same response, Repos.LastUpdated correctly reflected the most recent push, but NewestImage pointed at the old tag.

Root cause

zot selects a repo's "newest" image from the OCI created field of the config blob (GetImageLastUpdated): it reads created first, falls back to history, and otherwise returns zero time
(Jan 1, year 1). The repo's LastUpdatedImage pointer is only replaced when new.After(current).

trix publish declared the config with the standard OCI media type (application/vnd.oci.image.config.v1+json) but wrote a custom JSON with no created (only published_date, a field name zot
doesn't understand). As a result, every version resolved to zero time → zero.After(zero) == false → the second push never replaced the first, and the first-pushed tag stayed "newest"
forever.

Fields derived from the org.opencontainers.image.created annotation (which trix does set correctly) remained accurate — which is why ImageList and Repos.LastUpdated showed the right
order, producing the contradiction.

Fix

Derive an OCI-standard RFC3339 created from published_date and serialize it into the image config:

  • interfaces/oci.rs: new created: Option field on ImageMetadata + a created_timestamp(published_date) helper.
  • commands/publish.rs: the config blob now sets created.

The field uses #[serde(default, skip_serializing_if = "Option::is_none")], so artifacts published before this fix still pull without changes.

Testing / verification

  • TDD: a regression test that fails without the fix (config_blob_carries_oci_created_for_version_ordering) plus a backward-compatibility test (config_without_created_still_deserializes).
  • Full suite green (57/57), e2e compiles, no clippy findings in the changed code.
  • End-to-end against zot: after publishing a new tag with the patched binary, the config carries "created":"…" and the GlobalSearch { NewestImage } query correctly returns the newest
    version.

@mduthey mduthey requested a review from scarmuega June 22, 2026 17:55
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