Skip to content

Commit e40f755

Browse files
authored
Push artifacts as single-layer image directly to final ref (tinkerbell#37)
## Description GHCR requires packages:delete permission to remove tags, so the WIP tag cleanup was silently failing, leaving orphaned *-wip tags in the registry. Eliminate the WIP pattern entirely by bundling all artifact files into one tar layer and pushing straight to the final ref in a single crane append. Fixes: # ## How Has This Been Tested? ## How are existing users impacted? What migration steps/scripts do we need? ## Checklist: I have: - [ ] updated the documentation and/or roadmap (if required) - [ ] added unit or e2e tests - [ ] provided instructions on how to upgrade
2 parents d4dd483 + 2cb2206 commit e40f755

File tree

2 files changed

+12
-40
lines changed

2 files changed

+12
-40
lines changed

captain/crane.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Thin wrapper around the ``crane`` CLI for OCI image operations.
22
3-
Each artifact file is pushed as its own tar layer via ``crane append``,
3+
Artifact files are bundled into a tar and pushed via ``crane append``,
44
producing a valid OCI image with correct ``rootfs.diff_ids`` in the
55
config. This means:
66
@@ -135,10 +135,3 @@ def tag(src_ref: str, new_tag: str, *, logger: StageLogger | None = None) -> Non
135135
_log = logger or _default_log
136136
_log.log(f"crane tag {src_ref} {new_tag}")
137137
run(["crane", "tag", src_ref, new_tag])
138-
139-
140-
def delete(image_ref: str, *, logger: StageLogger | None = None) -> None:
141-
"""Delete *image_ref* from the registry."""
142-
_log = logger or _default_log
143-
_log.log(f"crane delete {image_ref}")
144-
run(["crane", "delete", image_ref])

captain/oci.py

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""High-level OCI artifact operations for publishing and retrieving releases.
22
3-
Each artifact file is stored as its own tar layer in a proper OCI image
4-
so that:
3+
Artifact files are bundled into a single tar layer and pushed as a valid
4+
OCI image so that:
55
66
* **containerd** can pull it (valid ``rootfs.diff_ids`` in the config) —
77
Kubernetes image-volume mounts work.
@@ -105,48 +105,27 @@ def publish(
105105
_log.err(f"Missing artifact: {f}")
106106
raise SystemExit(1)
107107

108-
# Build a multi-layer OCI image — one layer per artifact file.
109-
# Each layer is a tar containing a single file at the root,
110-
# so the image filesystem exposes individual files. crane computes
111-
# rootfs.diff_ids automatically, keeping containerd happy.
112-
#
113-
# Layers are appended to a temporary ref so that a partial failure
114-
# never leaves the final tag pointing at an incomplete image.
108+
# Bundle all artifact files into a single tar layer and push
109+
# directly to the final ref. crane computes rootfs.diff_ids
110+
# automatically, keeping containerd happy.
115111
ref = _image_ref(registry, repository, artifact_name, f"{tag}-{arch}")
116-
wip_ref = f"{ref}-wip"
117-
layer_tars: list[Path] = []
112+
layer_tar = out / ".layer-artifacts.tar"
118113
try:
119-
for i, f in enumerate(push_files):
120-
layer_tar = out / f".layer-{f.name}.tar"
121-
layer_tars.append(layer_tar)
122-
with tarfile.open(layer_tar, "w") as tf:
114+
with tarfile.open(layer_tar, "w") as tf:
115+
for f in push_files:
123116
tf.add(f, arcname=f.name)
124-
crane.append(
125-
layer_tar,
126-
wip_ref,
127-
base=wip_ref if i > 0 else None,
128-
logger=_log,
129-
)
130-
# All layers succeeded — set metadata and promote to the final tag.
117+
crane.append(layer_tar, ref, logger=_log)
131118
crane.mutate(
132-
wip_ref,
119+
ref,
133120
platform=f"linux/{arch}",
134121
annotations={
135122
"org.opencontainers.image.source": f"https://github.com/{repository}",
136123
"org.opencontainers.image.revision": sha,
137124
},
138-
tag=ref,
139125
logger=_log,
140126
)
141127
finally:
142-
# Best-effort cleanup of the temporary WIP tag (both on success
143-
# and failure) so partial builds don't accumulate in the registry.
144-
try:
145-
crane.delete(wip_ref, logger=_log)
146-
except subprocess.CalledProcessError:
147-
_log.log(f"Warning: could not delete temporary tag {wip_ref}")
148-
for t in layer_tars:
149-
t.unlink(missing_ok=True)
128+
layer_tar.unlink(missing_ok=True)
150129

151130
_log.log(f"Published {arch} artifacts → {ref}")
152131

0 commit comments

Comments
 (0)