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
7 changes: 7 additions & 0 deletions goal/push/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,13 @@ def _registry_publication_pending(project_types: List[str]) -> bool:
published = get_registry_version(registry, package)
if not published or published == current:
return False
# The registry's "latest" document is CDN-cached right after an upload, so
# a just-published tree can still look newer. Trust the version-specific
# document before claiming that publication is pending.
from goal.version_validation import exact_version_published

if exact_version_published(registry, package, current):
return False

def _key(version: str) -> tuple:
parts = re.findall(r"\d+", version.split("+")[0])
Expand Down
21 changes: 21 additions & 0 deletions goal/version_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,27 @@ def get_registry_version(registry: str, package_name: str) -> Optional[str]:
return None


def exact_version_published(
registry: str, package_name: str, version: str
) -> bool:
"""Return whether an exact version is already visible in the registry.

The registry "latest" document is CDN-cached for a short window after an
upload, so a tree that was just published can still look newer than the
registry. The version-specific document is the authoritative check for
that window: it answers whether this exact release already exists.
"""
if registry.lower() != "pypi" or not version:
return False
try:
url = f"https://pypi.org/pypi/{package_name}/{version}/json"
with urllib.request.urlopen(url, timeout=10) as response:
data = json.loads(response.read().decode())
return bool(data.get("info", {}).get("version") == version)
except (urllib.error.URLError, json.JSONDecodeError, KeyError):
return False


def extract_badge_versions(readme_path: Path) -> List[Tuple[str, str, str]]:
"""Extract version badges from README.md.

Expand Down
1 change: 1 addition & 0 deletions project/TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,4 +423,5 @@ This file indexes governance tickets without taking ownership of
| **ticket-210** | [`README.md`](./ticket-210/README.md) | - | - | - | - | - |
| **ticket-212** | [`README.md`](./ticket-212/README.md) | - | - | - | - | - |
| **ticket-211** | [`README.md`](./ticket-211/README.md) | - | - | - | - | - |
| **ticket-213** | [`README.md`](./ticket-213/README.md) | - | - | - | - | - |
<!-- AUTO:TICKET_INDEX:END -->
26 changes: 26 additions & 0 deletions project/ticket-213/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Ticket 213: Confirm exact registry version before treating tree as unpublished

- **ID**: ticket-213
- **Owner**: ai-claude
- **Status**: IN_PROGRESS
- **Workflow state**: EDIT
- **Created**: 2026-10-25

## Goal and scope

Observed live on heyasa right after the 0.4.0 completion: the registry's
"latest" JSON is CDN-cached, so a second `goal -a` in that window saw the tree
version as newer than the registry, re-entered the completion branch and failed
with the confusing clean force-publish error. The pending check now confirms
the exact version through the version-specific PyPI document before claiming
that publication is pending.

## Acceptance criteria

- [ ] AC-01: A CDN-lagged latest with the exact version published reports not pending; a genuinely unpublished version stays pending.
- [ ] AC-02: Existing completion/boundary tests still pass.

## Tracking boundary

This directory contains the minimal reviewed intent. Optional participant prose
and raw command logs are not required delivery output.
90 changes: 90 additions & 0 deletions project/ticket-213/intent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{
"schema": "new-project.intent/v3",
"ticket": "ticket-213",
"summary": "Confirm exact registry version before treating tree as unpublished",
"workstream": "application",
"classification": {
"kind": "BUG",
"priority": "P1",
"origin": "health"
},
"allowedPaths": [
"project/ticket-213/**",
"TODO.md",
"project/TICKETS.md",
"goal/push/core.py",
"goal/version_validation.py",
"tests/test_push_workflow_completion.py"
],
"forbiddenPaths": [
"project/ticket-*/user-*.md"
],
"stacks": [],
"dependsOn": [],
"conflictsWith": [],
"integrationTicket": null,
"delivery": {
"acceptedBaseSha": "086ee8f03ee8e00fa72b296a4f15000d31b76d09",
"targetBranch": "main",
"outcome": "Registry publication pending detection consults the version-specific PyPI document, so CDN lag after an upload no longer resurrects a finished publication or blocks a re-run.",
"nonGoals": [
"No changes to the publish transports",
"No changes to tag handling",
"No gate or policy changes"
],
"complexity": "S",
"estimatedMinutes": 25,
"budgets": {
"maxImplementationFiles": 3,
"maxAffectedComponents": 2,
"maxPublicInterfaceChanges": 0,
"maxRuntimeDependencies": 0
},
"architecture": {
"status": "accepted",
"decision": "Add exact_version_published() to version_validation and consult it inside _registry_publication_pending before returning pending.",
"components": [
{
"name": "push-workflow",
"paths": [
"goal/push/core.py",
"tests/test_push_workflow_completion.py"
]
},
{
"name": "registry-validation",
"paths": [
"goal/version_validation.py"
]
}
],
"responsibilityChanges": false,
"interfaceChanges": [],
"dataChanges": [],
"ui": {
"impact": "none",
"states": [],
"evidence": []
},
"rollback": "Protected revert of the three files; no data migration."
},
"runtimeDependencies": [],
"validation": [
{
"criterion": "AC-01",
"commands": [
"python -m pytest -q tests/test_push_workflow_completion.py -k exact_version"
],
"evidence": "receipt:goal213/exact-version"
},
{
"criterion": "AC-02",
"commands": [
"python -m pytest -q tests",
"./project/governance-check.sh"
],
"evidence": "receipt:goal213/regression"
}
]
}
}
29 changes: 29 additions & 0 deletions tests/test_push_workflow_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,11 @@ def test_registry_publication_pending_when_tree_ahead_of_registry(monkeypatch, t
"goal.version_validation.get_registry_version",
lambda registry, package: "2.2.1" if package == "goal" else None,
)
# The exact version is not published yet: the tree genuinely leads.
monkeypatch.setattr(
"goal.version_validation.exact_version_published",
lambda registry, package, version: False,
)

assert push_core._registry_publication_pending(["python"]) is True

Expand All @@ -392,6 +397,30 @@ def test_registry_publication_not_pending_when_registry_caught_up(monkeypatch, t
assert push_core._registry_publication_pending(["python"]) is False


def test_registry_publication_not_pending_when_exact_version_visible(monkeypatch, tmp_path):
"""CDN-lagged "latest" must not resurrect a finished publication."""
from types import SimpleNamespace

monkeypatch.chdir(tmp_path)
(tmp_path / "pyproject.toml").write_text(
'[project]\nname = "heyasa"\nversion = "0.4.0"\n', encoding="utf-8"
)
sources = [SimpleNamespace(value="0.4.0")]
monkeypatch.setattr(
"goal.cli.version_state.collect_version_sources", lambda: sources
)
monkeypatch.setattr(
"goal.version_validation.get_registry_version",
lambda registry, package: "0.3.1", # stale cached latest
)
monkeypatch.setattr(
"goal.version_validation.exact_version_published",
lambda registry, package, version: version == "0.4.0",
)

assert push_core._registry_publication_pending(["python"]) is False


def test_registry_publication_pending_requires_single_tree_version(monkeypatch, tmp_path):
from types import SimpleNamespace

Expand Down
Loading