Skip to content
153 changes: 152 additions & 1 deletion extensions/_oci.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ GET /v2/_oci/ext/discover
Repository-level extensions may be discovered with a standard GET as follows.

```HTTP
GET /v2/{name}/_oci/ext/discover
GET /v2/<name>/_oci/ext/discover
```

The base extension returns an array of supported extensions with details of the endpoints as shown below.
Expand Down Expand Up @@ -66,6 +66,157 @@ Content-Type: application/json

Enumeration of the endpoints provided on this registry (as not all "OPTIONAL" endpoints may be present in all registries)

### Component: `tag-history`

This component is for endpoints relating to tag history operations on a repository.

This endpoint returns the history of a tag, listing each manifest the tag has pointed to over time and each time the tag was deleted, in descending order (newest first).

Tag history MAY be retrieved with a standard `GET` as follows.

```HTTP
GET /v2/<name>/_oci/tag-history/<tag>
Comment thread
sudo-bmitch marked this conversation as resolved.
```

`<name>` is the namespace of the repository, and `<tag>` is the name of the tag whose history is being queried.

A successful request MUST return a `200 OK` response code.
If the registry does not implement this extension, the registry MUST return a `404 Not Found` response code.
The registry MUST NOT return a `404 Not Found` response code for any other condition.
If the repository does not exist or the tag has no history, the registry MUST return a `200 OK` response code with an empty manifest list.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the repository does not exist or the tag has no history

the statement is a bit ambiguous, a tag must have a history, at least created if it ever exits. It's better to reword to something like if the repository or tag never exits

Tag history SHOULD remain queryable after the tag is deleted.
Deleted manifests SHOULD NOT be removed from tag history.
Tag history MAY be deleted after the repository is deleted.
Comment on lines +83 to +89

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from the example below, it seems like each registry operation (push or delete) will only create a single entry in the history. it seems a little ambiguous reading these requirements whether a tag moving between images creates 1 created event or 2 events (deleted/created).

can we add a requirement to clarify that a tag move, or a push to an existing tag, will only produce a single "created" event referencing the new image digest, assuming that's the direction we want to go?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I can add clarifying language there - the intent was to do a single "create" event when a tag moves, and "deleted" events only apply if it is specifically removed and no longer referencable in the registry.


Upon success, the response body MUST be an OCI Index containing a list of descriptor objects in the following format:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not sure about image indexes being used to represent content that may/may not exist...

the referrers API was a pretty natural use of the image index since it's designed to contain content that's actually in the registry and is going to be pulled, but this feels forced -- any thoughts on using more of a bare bones JSON list, similar to #611?

potentially:

  GET /v2/team/web-app/_oci/tag-history/latest?n=3

  200 OK
  Content-Type: application/json

[
  {
    "event": "created",
    "timestamp": "2026-07-01T14:22:08.551204Z",
    "target": {
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "digest": "sha256:7d9f0e1c2b3a4958687776665554443332221110ffeeddccbbaa998877665544",
      "size": 3120
    }
  },
  {
    "event": "deleted",
    "timestamp": "2026-06-10T09:00:41.128870Z",
    "target": {
      "mediaType": "application/vnd.oci.image.index.v1+json",
      "digest": "sha256:e3a1c07f9b2d4856a0f1c2e3d4b5a69788c7d6e5f4039281a0b1c2d3e4f50617",
      "size": 5871
    }
  },
  {
    "event": "created",
    "timestamp": "2026-05-04T03:02:01.043916Z",
    "target": {
      "mediaType": "application/vnd.oci.image.index.v1+json",
      "digest": "sha256:e3a1c07f9b2d4856a0f1c2e3d4b5a69788c7d6e5f4039281a0b1c2d3e4f50617",
      "size": 5871
    }
  }
]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We debated this a couple times - ultimately opted to stick with the index approach rather than having to define a new schema/model that everyone needs to copy/import. The data format isn't ideal but at least it is known. Happy to revisit if more people challenge it but that was the consensus before.


```HTTP
200 OK
Content-Length: <length>
Content-Type: application/vnd.oci.image.index.v1+json

{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 1234,
"digest": "sha256:a1a1a1...",
"annotations": {
"org.opencontainers.distribution.tag.timestamp": "2026-05-04T03:02:01Z",
"org.opencontainers.distribution.tag.event": "created"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 1234,
"digest": "sha256:a1a1a1...",
"annotations": {
"org.opencontainers.distribution.tag.timestamp": "2026-02-03T03:02:01Z",
"org.opencontainers.distribution.tag.event": "deleted"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 1234,
"digest": "sha256:b2b2b2...",
"annotations": {
"org.opencontainers.distribution.tag.timestamp": "2025-04-03T02:01:00Z",
"org.opencontainers.distribution.tag.event": "created"
}
}
]
}
```

Results MUST be sorted in descending order by the history entries (i.e. the most recent entry appears first).

Each descriptor object in the response MUST be a create or delete entry.
A descriptor for a tag creation event MUST describe the manifest the tag was assigned to.
A descriptor for a tag deletion event MUST describe the manifest the tag pointed to immediately before deletion.

Manifest descriptors MUST include the following properties:
Comment thread
jcarter3 marked this conversation as resolved.

- **`mediaType`** *string*, REQUIRED

The media type of the manifest this tag pointed to at that point in time.

- **`size`** *integer*, REQUIRED

The size in bytes of the manifest.

- **`digest`** *string*, REQUIRED

The digest of the manifest, in the form `<algorithm>:<encoded>`.

- **`annotations`** *map of strings*, REQUIRED

Annotations associated with the historical tag entry. Registries MAY include annotations from the underlying manifest or index descriptor.
- When doing so, any existing annotations with keys beginning with `org.opencontainers.distribution` MUST be ignored because this prefix is reserved for annotations generated by the registry.

The annotations MUST include both of the following:

- **`org.opencontainers.distribution.tag.timestamp`** *string*

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timestamp is a universal concept, suggest remove tag from the annotation name


The RFC 3339 timestamp at which the tag history event occurred. Used as the sort key and as the cursor for time-based pagination.

- **`org.opencontainers.distribution.tag.event`** *string*

The tag history event type. MUST be either `created` when the tag was assigned to the manifest or `deleted` when the tag was deleted from the manifest.

##### Query Parameters

The following query parameters MAY be provided:
Comment thread
jcarter3 marked this conversation as resolved.

- **`n`** *integer*, OPTIONAL

Specifies the maximum number of results to return.
The registry MAY return fewer results than requested if fewer historical entries exist.
When `n` is zero, this endpoint MUST return an OCI Index with an empty manifest list and MUST NOT include a `Link` header, useful for determining whether the extension is supported (`200 OK`) or not (`404 Not Found`).
When `n` is not specified, the registry MAY apply a default limit.

- **`before`** *string (RFC 3339 timestamp)*, OPTIONAL

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe from or ot (older than)

Suggested change
- **`before`** *string (RFC 3339 timestamp)*, OPTIONAL
- **`ot`** *string (RFC 3339 timestamp)*, OPTIONAL

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a chance to have two results with the exact same time stamp..

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps a registry could consider every update of a tag as a delete followed by a create and so the timestamp would be the same.

@northtyphoon northtyphoon Aug 22, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would think the history can be more compact for repush case. Actually two created events are better repressing what acutally happened compared to created/deleted/created.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love the opposite of this (after or since) to support the use case of "tailing" changes such that a client can cheaply check for updates to a tag to stay up to date. Some similar mechanisms would be If-Modified-Since headers or something like RSS. It would be great if we could lean on HTTP primitives for this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated to include a since


When provided, only entries whose history entry timestamp is strictly less than (i.e. older than) `before` will be returned.
This is used for time-based pagination: pass the `org.opencontainers.distribution.tag.timestamp` value of the last entry returned in the previous response to retrieve the next page.

- **`since`** *string (RFC 3339 timestamp)*, OPTIONAL

When provided, only entries whose history entry timestamp is strictly greater than (i.e. newer than) `since` will be returned.
The `since` and `before` parameters MAY be combined to return entries within an open time interval.

- **`digest`** *string*, OPTIONAL

When provided, only history entries for the given digest will be returned.
If the digest does not exist or was not part of the given tag's history, the registry MUST return a `200 OK` response code with an empty manifest list.

Comment thread
jcarter3 marked this conversation as resolved.
##### Pagination Example

A `Link` header MUST be included in the response when additional history entries are available.
The `Link` header MUST be set according to [RFC 5988](https://www.rfc-editor.org/rfc/rfc5988) with the Relation Type `rel="next"`.
The target URI MAY contain registry-specific query parameters.
Clients MUST use the target URI as provided and MUST NOT modify or interpret registry-specific query parameters.
When a response does not include a `Link` header, the client has reached the end of the history.

To fetch the first page of up to 10 results:

```HTTP
GET /v2/<name>/_oci/tag-history/<tag>?n=10
```

When constructing the next request without using a `Link` header, pass the history entry timestamp of the last result from the prior response as the `before` parameter:

```HTTP
GET /v2/<name>/_oci/tag-history/<tag>?n=10&before=2026-04-03T02%3A01%3A00Z
```

To fetch all history entries after a given timestamp:

```HTTP
GET /v2/<name>/_oci/tag-history/<tag>?since=2026-04-01T00%3A00%3A00Z
```

## Code representations

Golang structures for these JSON structures is available at [`github.com/opencontainers/distribution-spec/specs-go/v1/extensions`](https://github.com/opencontainers/distribution-spec/tree/main/specs-go/v1/extensions/)
Expand Down
Loading