diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 5db4895..b8967c1 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -273,7 +273,7 @@ of the form `[@][#]`. `@` selects the registry; shell commands so `#` isn't treated as a comment. ```bash -# Run from default registry (latest tag, or default-branch HEAD if no tags) +# Run from default registry (default-branch HEAD) conductor run qa-bot # Run from a specific registry (latest) diff --git a/docs/design/registry.md b/docs/design/registry.md index b0b9744..ffe2c94 100644 --- a/docs/design/registry.md +++ b/docs/design/registry.md @@ -105,9 +105,9 @@ Resolution rules, in order: 3. Missing `@` → use the configured default registry. 4. An empty registry between `@` and `#` (e.g. `name@#ref`) is allowed and means "use the default registry at this ref". -5. Missing `#` → use `latest`. `latest` resolves to the newest - semver-sorted git tag (with a leading `v` stripped for parsing). If the - registry repo has no tags, `latest` falls back to the default branch HEAD. +5. Missing `#` → use `latest`. `latest` resolves to the **default + branch HEAD** of the registry repo (re-resolved to a fresh commit SHA + on every fetch). To pin to a release, use `#` explicitly. 6. An empty ref after `#` (e.g. `name@reg#`) is a hard error. 7. Multiple `@` or multiple `#` in a single reference are hard errors. 8. Path-type registries do not support `#`. Passing @@ -154,12 +154,13 @@ auto-discover YAML files in a registry — the maintainer curates the index. Versioning is automatic and tag-driven for GitHub registries: - **Auto-discovery**: available versions are the registry repo's git tags, - fetched on demand via the GitHub API. Maintainers do not list versions in - `index.yaml`. -- **`latest` resolution**: `latest` resolves to the newest semver-sorted tag - (a leading `v` is stripped before parsing, so `v1.2.3` and `1.2.3` sort - identically). If the repo has no tags, `latest` falls back to the - default-branch HEAD. + fetched on demand via the GitHub API for display in `registry list`/`show`. + Maintainers do not list versions in `index.yaml`. +- **`latest` resolution**: `latest` (the default when no `#` is given) + always resolves to the **default branch HEAD** of the registry repo. + This means a bare `name@registry` reference always picks up the newest + commit on the default branch — typical for development workflows. + To pin to a tagged release, use an explicit ref: `name@registry#v1.2.3`. - **Flexible refs**: any tag, branch, or commit SHA can be pinned via `#`. Branch refs are re-resolved to their current commit SHA at fetch time, so a branch ref always refers to the latest commit on that @@ -288,7 +289,7 @@ file of the same name. | Decision | Choice | Why | | ----------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | Named registries vs always-inline source | Named, configured once | Mirrors npm/cargo. Short refs in `run` commands. Default registry makes the common case zero-config. | -| Versioning | Auto-discovered from git tags; pin any tag/branch/SHA via `#ref` | Reproducibility without forcing maintainers to maintain a parallel version list. `latest` follows newest semver tag, falling back to default-branch HEAD. Branches and SHAs are first-class refs. | +| Versioning | Default branch HEAD when unpinned; pin any tag/branch/SHA via `#ref` | Bare names always follow the default branch (typical dev workflow). Tagged releases are opt-in via explicit `#`. Avoids the surprise of `latest` skipping past commits because a tag exists. Branches and SHAs are first-class refs. | | Local-registry layout | Directory + `index.yaml` | Consistent with GitHub registries. Maintainer controls what's exposed. Local registries do not support refs. | | Caching strategy | Local cache keyed by resolved commit SHA, atomic writes | Avoids per-run network. SHA-based keys make branch refs self-invalidate on a fresh fetch. SHA-pinned raw URLs bypass the CDN, so no `--force` flag is needed. | | Reference syntax | `name@registry#ref` | Visually unambiguous: `@` selects the registry, `#` selects a git ref (tag, branch, or SHA). Both segments are independently optional. | diff --git a/src/conductor/registry/version_resolver.py b/src/conductor/registry/version_resolver.py index 90194c4..6d6b855 100644 --- a/src/conductor/registry/version_resolver.py +++ b/src/conductor/registry/version_resolver.py @@ -8,7 +8,6 @@ from conductor.registry.errors import RegistryError from conductor.registry.github import ( get_default_branch, - list_tags, parse_github_source, resolve_ref_to_sha, ) @@ -21,11 +20,14 @@ def resolve_ref(entry: RegistryEntry, requested: str | None) -> str: (path registries do not support refs). For github registries: - * If ``requested`` is None or "latest", returns the newest tag - (semver-sorted when possible). If no tags exist, returns the default - branch name. - * Otherwise returns ``requested`` verbatim — this allows pinning to any - tag, branch, or commit SHA. + * If ``requested`` is None or "latest", returns the default branch + name. The caller will materialize this to the current commit SHA, + so users always pick up the newest commit on the default branch. + * Otherwise returns ``requested`` verbatim — this allows pinning to + any tag, branch, or commit SHA. + + Tags are no longer auto-selected as "latest"; users who want a specific + release should pin explicitly with ``#``. """ if entry.type == RegistryType.path: if requested is not None and requested != "": @@ -40,9 +42,6 @@ def resolve_ref(entry: RegistryEntry, requested: str | None) -> str: if requested is None or requested.lower() == "latest": owner, repo = parse_github_source(entry.source) - tags = list_tags(owner, repo) - if tags: - return sort_tags(tags)[0] return get_default_branch(owner, repo) return requested diff --git a/tests/test_registry/test_version_resolver.py b/tests/test_registry/test_version_resolver.py index 5525e15..01d9632 100644 --- a/tests/test_registry/test_version_resolver.py +++ b/tests/test_registry/test_version_resolver.py @@ -48,38 +48,28 @@ def test_resolve_ref_path_with_ref_raises() -> None: # --------------------------------------------------------------------------- -def test_resolve_ref_github_none_picks_newest_tag(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr( - version_resolver, "list_tags", lambda owner, repo: ["v1.0.0", "v2.0.0", "v1.1.0"] - ) - monkeypatch.setattr( - version_resolver, - "get_default_branch", - lambda owner, repo: pytest.fail("should not be called"), - ) - assert resolve_ref(_gh_entry(), None) == "v2.0.0" +def test_resolve_ref_github_none_returns_default_branch(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(version_resolver, "get_default_branch", lambda owner, repo: "main") + assert resolve_ref(_gh_entry(), None) == "main" def test_resolve_ref_github_latest_same_as_none(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr( - version_resolver, "list_tags", lambda owner, repo: ["v1.0.0", "v2.0.0", "v1.1.0"] - ) - assert resolve_ref(_gh_entry(), "latest") == "v2.0.0" - assert resolve_ref(_gh_entry(), "LATEST") == "v2.0.0" + monkeypatch.setattr(version_resolver, "get_default_branch", lambda owner, repo: "trunk") + assert resolve_ref(_gh_entry(), "latest") == "trunk" + assert resolve_ref(_gh_entry(), "LATEST") == "trunk" -def test_resolve_ref_github_no_tags_returns_default_branch( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setattr(version_resolver, "list_tags", lambda owner, repo: []) +def test_resolve_ref_github_does_not_query_tags(monkeypatch: pytest.MonkeyPatch) -> None: + """Default-branch resolution must not require a tag listing API call.""" monkeypatch.setattr(version_resolver, "get_default_branch", lambda owner, repo: "main") + # If resolve_ref ever imports list_tags again, this test will surface it. assert resolve_ref(_gh_entry(), None) == "main" def test_resolve_ref_github_branch_returned_verbatim(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr( version_resolver, - "list_tags", + "get_default_branch", lambda owner, repo: pytest.fail("should not be called"), ) assert resolve_ref(_gh_entry(), "main") == "main" @@ -88,7 +78,7 @@ def test_resolve_ref_github_branch_returned_verbatim(monkeypatch: pytest.MonkeyP def test_resolve_ref_github_tag_returned_verbatim(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr( version_resolver, - "list_tags", + "get_default_branch", lambda owner, repo: pytest.fail("should not be called"), ) assert resolve_ref(_gh_entry(), "v1.0.0") == "v1.0.0"