The release flow computes a release-range (latest tag..HEAD) and refuses
when the latest release tag is not an ancestor of HEAD. When the operating
checkout was materialized without tags (or shallow), the tag is absent or
its connecting history is missing, so a genuinely-reachable tag is reported
as "not reachable from HEAD" and release is fully blocked (#6916).
Add git::fetch_tags() and call it at the entry of resolve_tag_and_commits,
before the reachability/changelog-range guard runs. It fetches tags from the
resolved default remote and unshallows a shallow checkout so ancestry can be
computed accurately. It is best-effort: an offline checkout still falls
through to the guard against local history, and the guard correctness is
preserved — a tag that is genuinely not an ancestor of HEAD is still refused.
Secondary: the batch summary rolled up any non-skipped Ok component as
"released", even when its authoritative per-component status was failed/
missing/partial, producing a misleading {released, failed} summary. Roll up
the real per-component status via batch_status_bucket so only a clean
"released" counts as released; every other non-skipped outcome counts as
failed.
Tests: a tagless checkout with the tag reachable on origin now proceeds past
the guard; an off-branch tag still fails closed after fetching tags; the
batch bucket maps failed/missing/partial/unknown to failed.
Closes #6916
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes #6916
Problem
homeboy release <component>refuses with:even when the tag genuinely IS reachable from HEAD in the real repo. The release flow computes a release-range (
latest tag..HEAD) and requires the latest release tag be an ancestor of HEAD. When the operating checkout was materialized without tags (or shallow), the tag is either absent or its connecting commits are missing, sogit merge-base --is-ancestor <tag> HEADreports false and the guard refuses — fully blocking releases.Fix 1 — make tags available before the guard (guard correctness preserved)
The reachability/changelog-range guard lives in
validate_latest_release_tag_reachable, called from the single chokepointresolve_tag_and_commits(which also computes the latest tag and the commit range). I addedgit::fetch_tags()and call it at the entry ofresolve_tag_and_commits, before the guard:git fetch --tags <remote>).git fetch --unshallow --tags) so ancestry can be computed accurately.The fix makes tags available so the check is accurate — it does not skip or weaken the check. A tag that is genuinely not an ancestor of HEAD is still refused.
Fix 2 — batch summary rollup
run_batchlabeled any non-skippedOkcomponent asreleased, even when its authoritative per-component status wasfailed/missing/partial. That produced the misleading{"released": 1, "failed": 1}summary in the issue. The rollup now reads the realresult.statusvia a newbatch_status_bucket: only a cleanreleasedcounts as released;skippedis its own bucket; every other non-skipped outcome (failed/missing/partial/unknown) counts as failed so the summary never overstates success.Tests
resolve_tag_and_commits_fetches_missing_tag_reachable_on_origin— a tagless checkout (cloned--no-tagsfrom a local bare-ish origin) where the tag is reachable on origin now proceeds past the guard (no network: uses a local fixture origin).resolve_tag_and_commits_still_refuses_genuinely_unreachable_tag_after_fetch— an off-branch tag fetched from origin still fails closed (guard correctness preserved).batch_status_bucket_rolls_up_non_released_outcomes_as_failed— failed/missing/partial/unknown roll up as failed, not released.resolve_tag_and_commits_fails_closed_when_latest_release_tag_is_off_branchstill passes (fetch_tagsis a no-op without a remote).cargo buildclean;cargo test --lib release::304 passed single-threaded. (One env-dependent enterprise-proxy test ingithub_releaseis flaky under parallelism on both base and branch — unrelated to this change.)AI assistance