Skip to content
ocx
install

Authoring packages: announce

Part 2 of 2 of Authoring packages.

announce — publish the listing to the index

Section titled “announce — publish the listing to the index”

announce writes the package’s tag listing into the ocx index on a forge (GitHub or GitLab) and returns an AnnounceReport — status is "updated" or "unchanged", pull_request_url is set when the forge path went through a PR, capability_checks records what the credential was allowed to do. Exactly one tag source is required — tags=, tags_file= (the file cascade_repair(announce_tags=...) writes) or tags_from_registry=True — and ocx, not the SDK, enforces that: none or two is a UsageError (64).

# illustrative: needs a forge credential and network access.
from ocx_sdk import Ocx, OcxConfig
ocx = Ocx(config=OcxConfig(forge_token="glpat-..."))
report = ocx.package.announce("me/my-tool", tags=["1.0.0", "latest"])
print(report.status, report.pull_request_url)

A registry write like push, so retry defaults to None. The credential comes from OcxConfig.forge_token (OCX_ANNOUNCE_TOKEN) or the ambient identity ladder described in Hermetic CI. --yank, --unyank, --fork and --out are not typed: reach them through invoke.

claim — register a package with the index

Section titled “claim — register a package with the index”

claim registers a package name and its owners in the index, returning a ClaimReport. repository is the OCI repository the claim points at; owners= lists forge logins beyond the caller.

A claim is not idempotent on the wire: claiming an already-claimed package exits 65 with no report, which the SDK surfaces as a plain DataError. The idempotent-CI shape is to catch it and read detail off the error envelope — "package_already_claimed", a frozen slug, never the message text:

# illustrative: needs a forge credential and network access.
from ocx_sdk import DataError, Ocx, error_envelope
try:
Ocx().package.claim("me/my-tool", repository="oci://ghcr.io/me/my-tool")
except DataError as exc:
envelope = error_envelope(exc)
if envelope is None or envelope.detail != "package_already_claimed":
raise

ocx’s command reference tables the rest of claim’s slugs (malformed_repository, owner_unknown, bot_identity, …), each pinned to its exit code.

cascade_check / cascade_repair — audit the rolling tags

Section titled “cascade_check / cascade_repair — audit the rolling tags”

push(cascade=True) advances the rolling tags (1, 1.0, latest) above the version it publishes; a push without it leaves them behind. cascade_check audits one or more repositories and cascade_repair rewrites what is stale. Both are report-then-fail: ocx exits 65 with the report whenever it found something, and the SDK hands that report back as a result rather than raising — the finding is the answer. clean is the one-word verdict; exit_code keeps what ocx said.

from ocx_sdk import CascadeCheckReport
report = CascadeCheckReport.from_json(
'{"reports": [{"identifier": "ghcr.io/me/my-tool", "logical": null, "aliases": {}, '
'"rows": [{"tag": "latest", "platform": {}, "status": "stale", "observed": null, "expected": null, '
'"source": "1.0.1", "observed_source": "1.0.0"}], '
'"index_findings": [], "ignored_tags": [], "unrepairable": []}]}',
exit_code=65,
)
assert not report.clean
assert [(row.tag, row.status) for row in report.reports[0].rows] == [("latest", "stale")]

cascade_repair(dry_run=True, announce_tags=path) plans without writing and leaves the tags it would touch in path — the file announce(tags_file=path) consumes. Only a 65 without a report (an error envelope instead) raises.

The loop closes through the ordinary consumer surface — package.install or a project’s add — pointed at the identifier push returned. See Vendoring a dist.json for shipping a bootstrap manifest snapshot inside a package, which is a separate concern from publishing the package itself.