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
23 changes: 23 additions & 0 deletions goal/push/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,17 @@ def _has_committed_unreleased_sources() -> bool:
return _registry_publication_pending(project_types)


def _managed_version_values() -> List[str]:
"""Return the distinct managed version declarations in the tree."""
try:
from goal.cli.version_state import collect_version_sources

sources = collect_version_sources()
except (ImportError, OSError, ValueError):
return []
return sorted({source.value for source in sources if source.value is not None})


def _registry_publication_pending(project_types: List[str]) -> bool:
"""Return whether the tree version is newer than the published registry."""
if not project_types:
Expand Down Expand Up @@ -763,6 +774,18 @@ def execute_push_workflow(
if delivery is not None:
ticket = resolve_pull_request_ticket(delivery, ticket)
if delivery.mode == "pull-request" and ticket is None:
disagreeing = _managed_version_values()
if len(disagreeing) > 1:
# A coordinated release lands its version projection across
# workstream tickets; between them the managed declarations
# disagree. Name that exact state instead of a generic
# release-ticket or clean force-publish message.
raise click.ClickException(
"Managed version sources disagree ("
+ ", ".join(disagreeing)
+ "): land the remaining coordinated release tickets "
"(version files and runtime banner) before running goal -a."
)
if _has_committed_unreleased_sources():
if not _registry_publication_pending(
list(detect_project_types_deep().keys())
Expand Down
1 change: 1 addition & 0 deletions project/TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,4 +425,5 @@ This file indexes governance tickets without taking ownership of
| **ticket-211** | [`README.md`](./ticket-211/README.md) | - | - | - | - | - |
| **ticket-213** | [`README.md`](./ticket-213/README.md) | - | - | - | - | - |
| **ticket-215** | [`README.md`](./ticket-215/README.md) | - | - | - | - | - |
| **ticket-216** | [`README.md`](./ticket-216/README.md) | - | - | - | - | - |
<!-- AUTO:TICKET_INDEX:END -->
26 changes: 26 additions & 0 deletions project/ticket-216/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Ticket 216: Diagnose disagreeing managed version sources explicitly

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

## Goal and scope

A coordinated governed release lands its version projection across workstream
tickets (integration owns VERSION/pyproject, application owns the runtime
banner). Between those merges the managed declarations disagree; `goal -a`
answered with a generic "requires a release ticket" or the confusing clean
force-publish error. The pull-request boundary now names the exact state and
the remaining action.

## Acceptance criteria

- [ ] AC-01: Disagreeing managed versions raise "Managed version sources disagree (...)" naming the values and the remaining tickets.
- [ ] AC-02: Single-version behavior (pending completion, unreleased sources, no-change) is unchanged.

## Tracking boundary

This directory contains the minimal reviewed intent. Optional participant prose
and raw command logs are not required delivery output.
82 changes: 82 additions & 0 deletions project/ticket-216/intent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"schema": "new-project.intent/v3",
"ticket": "ticket-216",
"summary": "Diagnose disagreeing managed version sources explicitly",
"workstream": "application",
"classification": {
"kind": "BUG",
"priority": "P2",
"origin": "health"
},
"allowedPaths": [
"project/ticket-216/**",
"TODO.md",
"project/TICKETS.md",
"goal/push/core.py",
"tests/test_push_workflow_completion.py"
],
"forbiddenPaths": [
"project/ticket-*/user-*.md"
],
"stacks": [],
"dependsOn": [],
"conflictsWith": [],
"integrationTicket": null,
"delivery": {
"acceptedBaseSha": "ffb8cb135f8effe2d7129c0a8d9cdb708cafb0ee",
"targetBranch": "main",
"outcome": "The governed pull-request boundary reports disagreeing managed version sources explicitly, naming the values and the remaining coordinated release tickets instead of a generic message.",
"nonGoals": [
"No change to version synchronization itself",
"No gate or policy changes"
],
"complexity": "S",
"estimatedMinutes": 20,
"budgets": {
"maxImplementationFiles": 2,
"maxAffectedComponents": 1,
"maxPublicInterfaceChanges": 0,
"maxRuntimeDependencies": 0
},
"architecture": {
"status": "accepted",
"decision": "Add _managed_version_values() and guard the ticket-less pull-request boundary before the pending/no-change branches.",
"components": [
{
"name": "push-workflow",
"paths": [
"goal/push/core.py",
"tests/test_push_workflow_completion.py"
]
}
],
"responsibilityChanges": false,
"interfaceChanges": [],
"dataChanges": [],
"ui": {
"impact": "none",
"states": [],
"evidence": []
},
"rollback": "Protected revert of the two files; no data migration."
},
"runtimeDependencies": [],
"validation": [
{
"criterion": "AC-01",
"commands": [
"python -m pytest -q tests/test_push_workflow_completion.py -k disagreeing"
],
"evidence": "receipt:goal216/conflict-message"
},
{
"criterion": "AC-02",
"commands": [
"python -m pytest -q tests/test_push_workflow_completion.py",
"./project/governance-check.sh"
],
"evidence": "receipt:goal216/regression"
}
]
}
}
45 changes: 45 additions & 0 deletions tests/test_push_workflow_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ def _run_workflow_until_project_detection(monkeypatch, ctx, recorded, sources_pe
"goal.governance.delivery.resolve_pull_request_ticket",
lambda policy, ticket, **kwargs: None,
)
monkeypatch.setattr(
push_core, "_managed_version_values", lambda: ["2.2.5"]
)
monkeypatch.setattr(
push_core,
"_has_committed_unreleased_sources",
Expand Down Expand Up @@ -238,6 +241,45 @@ def test_is_governed_target_helper_stays_manifest_bound(tmp_path, monkeypatch):
assert _is_governed_target() is True


def test_disagreeing_managed_versions_are_named_explicitly(monkeypatch):
"""A mid-release projection split gets its own actionable diagnostic."""
import click

ctx = _workflow_ctx()
monkeypatch.setattr(push_core, "_validate_toml_or_exit", lambda dry_run: None)
monkeypatch.setattr(
"goal.governance.delivery.resolve_delivery_policy",
lambda *a, **k: _pull_request_policy(),
)
monkeypatch.setattr(
"goal.governance.delivery.validate_delivery_ready", lambda delivery: None
)
monkeypatch.setattr(
"goal.governance.delivery.resolve_pull_request_ticket",
lambda policy, ticket, **kwargs: None,
)
monkeypatch.setattr(
push_core, "_managed_version_values", lambda: ["2.2.4", "2.2.5"]
)

with pytest.raises(click.ClickException, match="Managed version sources disagree"):
push_core.execute_push_workflow(
ctx,
bump=None,
no_tag=False,
no_changelog=False,
no_version_sync=False,
message=None,
dry_run=False,
yes=True,
markdown=False,
split=False,
ticket=None,
abstraction=None,
todo=False,
)


def test_unreleased_sources_without_prebumped_version_name_the_route(monkeypatch):
"""Unreleased work after the last tag is not a pending publish completion."""
import click
Expand All @@ -260,6 +302,9 @@ def test_unreleased_sources_without_prebumped_version_name_the_route(monkeypatch
"goal.governance.delivery.resolve_pull_request_ticket",
lambda policy, ticket, **kwargs: None,
)
monkeypatch.setattr(
push_core, "_managed_version_values", lambda: ["2.2.5"]
)
monkeypatch.setattr(
push_core, "_has_committed_unreleased_sources", lambda: True
)
Expand Down
Loading