Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/actions/delete-previous-head-image/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ removes exactly that image after the new one is pushed:
and each manifest in it

It keeps anything that is still reachable from another tag, such as a release
whose image shares a manifest, together with that manifest's signatures and
attestations, and any version that carries a tag it did not
expect. If the previous image is still tagged (for example, promoted to a
whose image shares a manifest, and any version that carries a tag it did not
expect. Every manifest it keeps also keeps its signatures and attestations,
whichever reason kept it. If the previous image is still tagged (for example, promoted to a
release), it deletes nothing. It checks that against both the registry tag list
and the packages API, and if either shows a tag, it deletes nothing. Every deleted
and kept version is logged and written to the step summary.
Expand Down
28 changes: 14 additions & 14 deletions .github/actions/delete-previous-head-image/src/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,11 +240,6 @@ def delete(owner, name, token, previous, current, dry_run):
if previous in protected:
log(f"{previous} is still tagged {protected[previous]}; nothing to delete")
return
# A kept manifest keeps its signatures and attestations too.
for digest, subject in signs.items():
if subject in protected:
protected.setdefault(digest, protected[subject])

packages = Packages(owner, name, token)
versions = packages.versions(set(tree))
# The registry and the packages API are separate reads; trust the stricter one.
Expand All @@ -255,17 +250,22 @@ def delete(owner, name, token, previous, current, dry_run):
tags = ", ".join(sorted(versions[previous][1]))
log(f"{previous} is still tagged {tags}; nothing to delete")
return
plan, kept = [], []
kept = {}
for digest, allowed in tree.items():
if digest in protected:
kept.append(f"{digest}: also part of tag {protected[digest]}")
kept[digest] = f"also part of tag {protected[digest]}"
elif digest not in versions:
kept.append(f"{digest}: no package version found")
kept[digest] = "no package version found"
elif not versions[digest][1] <= allowed:
extra = ", ".join(sorted(versions[digest][1] - allowed))
kept.append(f"{digest}: carries tag {extra}")
else:
plan.append((digest, versions[digest][0]))
kept[digest] = f"carries tag {extra}"

# A kept manifest keeps its signatures and attestations too, including when
# only the packages API revealed the reason to keep it.
for digest, subject in signs.items():
if subject in kept:
kept.setdefault(digest, f"refers to kept manifest {subject}")
plan = [(digest, versions[digest][0]) for digest in tree if digest not in kept]

verb = "would delete" if dry_run else "deleting"
lines = [f"### Previous head image {previous}"]
Expand All @@ -274,9 +274,9 @@ def delete(owner, name, token, previous, current, dry_run):
lines.append(f"- {verb} `{digest}` (version {version_id})")
if not dry_run:
packages.delete(version_id)
for reason in kept:
log(f"keeping {reason}")
lines.append(f"- keeping {reason}")
for digest, reason in kept.items():
log(f"keeping {digest}: {reason}")
lines.append(f"- keeping {digest}: {reason}")
summary(lines)


Expand Down
27 changes: 27 additions & 0 deletions .github/actions/delete-previous-head-image/test/test_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,33 @@ def test_version_with_an_unexpected_tag_is_kept(fake, tmp_path):
assert "carries tag debug" in result.stdout


@pytest.mark.parametrize("dry_run", [False, True])
def test_packages_only_child_tag_keeps_its_referrers(fake, tmp_path, dry_run):
old, new, manifests, tags, versions = candy()
child = d("1")
referrer_tag = "sha256-" + child[7:]
manifests[d("9")] = index(d("8"))
manifests[d("8")] = image()
tags[referrer_tag] = d("9")
server = fake(manifests=manifests, tags=tags)
server.versions[1] = version(2, child, "debug")
server.versions += [version(13, d("9"), referrer_tag), version(14, d("8"))]

result = run(
server, tmp_path, mode="delete", previous_digest=old, current_digest=new,
dry_run=str(dry_run),
)

assert result.returncode == 0, result.stdout
assert sorted(server.deleted) == ([] if dry_run else [1, 3, 4, 5])
verb = "would delete" if dry_run else "deleting"
for digest in (old, d("2"), d("3"), d("e")):
assert f"{verb} {digest}" in result.stdout
for digest in (child, d("9"), d("8")):
assert f"{verb} {digest}" not in result.stdout
assert "carries tag debug" in result.stdout


def test_no_previous_digest_is_a_no_op(fake, tmp_path):
server = fake()
result = run(
Expand Down
Loading