Skip to content
This repository was archived by the owner on Aug 12, 2026. It is now read-only.
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
2 changes: 0 additions & 2 deletions src/embedder/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ def parse_ref(self, raw: str) -> AnyRef: ...

def resolve(self, ref: AnyRef) -> AnyRef: ...

def always_refresh(self, ref: AnyRef) -> bool: ...

def fetch(self, ref: AnyRef, base_dir: Path) -> str: ...


Expand Down
30 changes: 4 additions & 26 deletions src/embedder/providers/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,26 @@
r"^github\.com/"
r"(?P<owner>[A-Za-z0-9_.-]+)/"
r"(?P<repo>[A-Za-z0-9_.-]+)"
r"(?:@(?P<tag>[^:\s]+))?"
r"@(?P<tag>[^:\s]+)"
r":(?P<asset>[^\s]+)$"
)

_SEMVER_RE = re.compile(r"^v?\d+(\.\d+)*$")


@dataclass(frozen=True)
class GitHubAssetRef:
owner: str
repo: str
asset: str
tag: str | None = None
tag: str

@property
def repository(self) -> str:
return f"{self.owner}/{self.repo}"

@property
def is_pinned(self) -> bool:
return self.tag is not None and bool(_SEMVER_RE.match(self.tag))

def with_tag(self, tag: str) -> GitHubAssetRef:
return GitHubAssetRef(owner=self.owner, repo=self.repo, asset=self.asset, tag=tag)

def render(self) -> str:
if self.tag is None:
return f"github.com/{self.repository}:{self.asset}"
return f"github.com/{self.repository}@{self.tag}:{self.asset}"


Expand All @@ -59,7 +51,7 @@ def parse_github_ref(raw: str) -> GitHubAssetRef:
owner=match["owner"],
repo=match["repo"],
asset=asset,
tag=match.group("tag"),
tag=match["tag"],
)


Expand All @@ -78,14 +70,9 @@ def parse_ref(self, raw: str) -> GitHubAssetRef:
return parse_github_ref(raw)

def resolve(self, ref: GitHubAssetRef) -> GitHubAssetRef:
# Validate gh is available for any GitHub ref, then return as-is.
# Tagless and branch refs are handled via always_refresh + fetch.
self.require()
return ref

def always_refresh(self, ref: GitHubAssetRef) -> bool:
return not ref.is_pinned

def fetch(self, ref: GitHubAssetRef, base_dir: Path) -> str:
return self._fetch_file(ref)

Expand Down Expand Up @@ -116,18 +103,9 @@ def run(self, args: list[str], *, check: bool = True) -> CommandResult:
def auth_ok(self) -> bool:
return self.run(["auth", "status"], check=False).returncode == 0

def _latest_tag(self, ref: GitHubAssetRef) -> str:
result = self.run(
["api", f"repos/{ref.repository}/releases/latest", "--jq", ".tag_name"]
)
if not result.stdout or result.stdout == "null":
raise EmbedderError(f"Could not resolve latest release for {ref.repository}")
return result.stdout

def _fetch_file(self, ref: GitHubAssetRef) -> str:
tag = ref.tag if ref.tag is not None else self._latest_tag(ref)
encoded_asset = quote(ref.asset, safe="/")
encoded_tag = quote(tag, safe="")
encoded_tag = quote(ref.tag, safe="")
result = self.run(
[
"api",
Expand Down
3 changes: 0 additions & 3 deletions src/embedder/providers/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ def parse_ref(self, raw: str) -> LocalRef:
def resolve(self, ref: LocalRef) -> LocalRef:
return ref

def always_refresh(self, ref: LocalRef) -> bool:
return True

def fetch(self, ref: LocalRef, base_dir: Path) -> str:
resolved_base = base_dir.resolve()
target = (base_dir / ref.path).resolve()
Expand Down
4 changes: 1 addition & 3 deletions src/embedder/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,8 @@ def update_files(
provider = get_provider(check.block.ref.render(), _providers)
if local_only and not isinstance(provider, LocalProvider):
continue
if not check.update_available and not provider.always_refresh(check.block.ref):
continue
new_body = provider.fetch(check.latest_ref, _base_dir)
if new_body == check.block.body and not check.update_available:
if new_body == check.block.body:
continue
updates.append(
BlockUpdate(block=check.block, new_ref=check.block.ref, new_body=new_body)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def test_check_missing_gh_returns_environment_error(
target.write_text(
"\n".join(
[
marker("github.com/rubykatzen/embedder:fragment.md"),
marker("github.com/rubykatzen/embedder@v0.1.0:fragment.md"),
"managed",
close_marker(),
"",
Expand Down
15 changes: 10 additions & 5 deletions tests/test_refs.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,9 @@ def test_reject_invalid_github_ref() -> None:
parse_github_ref("rubykatzen/embedder@v0.1.0:fragment.md")


def test_parse_github_ref_without_tag() -> None:
ref = parse_github_ref("github.com/OWNER/repo-name:file.md")

assert ref.tag is None
assert ref.render() == "github.com/OWNER/repo-name:file.md"
def test_reject_tagless_github_ref() -> None:
with pytest.raises(RefError):
parse_github_ref("github.com/OWNER/repo-name:file.md")


def test_parse_github_ref_branch() -> None:
Expand All @@ -43,6 +41,13 @@ def test_parse_github_ref_branch() -> None:
assert ref.render() == "github.com/rubykatzen/embedder@main:docs/fragment.md"


def test_parse_github_ref_floating_tag() -> None:
ref = parse_github_ref("github.com/rubykatzen/embedder@v0.2:fragments/file.md")

assert ref.tag == "v0.2"
assert ref.render() == "github.com/rubykatzen/embedder@v0.2:fragments/file.md"


def test_asset_path_with_subdirectory() -> None:
ref = parse_github_ref("github.com/rubykatzen/embedder@v0.1.0:docs/fragments/file.md")

Expand Down
38 changes: 8 additions & 30 deletions tests/test_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ def resolve(self, ref: GitHubAssetRef) -> GitHubAssetRef:
self.resolve_calls += 1
return ref

def always_refresh(self, ref: GitHubAssetRef) -> bool:
return not ref.is_pinned

def fetch(self, ref: GitHubAssetRef, base_dir: Path) -> str:
return self.contents[(ref.repository, ref.asset)]

Expand All @@ -41,32 +38,13 @@ def fake_providers() -> list[Provider]:
return [FakeGitHubProvider(), LocalProvider()]


def test_check_blocks_tagless_ref_not_update_pending() -> None:
"""Auto-latest refs are always-refresh; check() doesn't report them as pending updates."""
text = "\n".join(
[
marker("github.com/rubykatzen/embedder:fragment.md"),
"old",
close_marker(),
"",
]
)
blocks = parse_blocks(Path("AGENTS.md"), text)

results = check_blocks(blocks, fake_providers())

assert len(results) == 1
assert not results[0].update_available
assert results[0].latest_ref.render() == "github.com/rubykatzen/embedder:fragment.md"


def test_update_files_replaces_only_managed_body(tmp_path: Path) -> None:
target = tmp_path / "AGENTS.md"
target.write_text(
"\n".join(
[
"before",
marker("github.com/rubykatzen/embedder:fragment.md"),
marker("github.com/rubykatzen/embedder@v0.1.0:fragment.md"),
"old managed text",
close_marker(),
"after",
Expand All @@ -82,7 +60,7 @@ def test_update_files_replaces_only_managed_body(tmp_path: Path) -> None:
assert target.read_text(encoding="utf-8") == "\n".join(
[
"before",
marker("github.com/rubykatzen/embedder:fragment.md"),
marker("github.com/rubykatzen/embedder@v0.1.0:fragment.md"),
"new managed text",
close_marker(),
"after",
Expand Down Expand Up @@ -125,10 +103,10 @@ def test_check_blocks_calls_resolve_per_block() -> None:
"""resolve() is called once per block (no caching); it's a no-op for all ref types."""
text = "\n".join(
[
marker("github.com/rubykatzen/embedder:first.md"),
marker("github.com/rubykatzen/embedder@v0.1.0:first.md"),
"old",
close_marker(),
marker("github.com/rubykatzen/embedder:second.md"),
marker("github.com/rubykatzen/embedder@v0.1.0:second.md"),
"old",
close_marker(),
"",
Expand Down Expand Up @@ -179,10 +157,10 @@ def test_cache_uses_correct_asset_per_block() -> None:
"""Two blocks from the same repo must not share each other's asset."""
text = "\n".join(
[
marker("github.com/rubykatzen/embedder:first.md"),
marker("github.com/rubykatzen/embedder@v0.1.0:first.md"),
"old",
close_marker(),
marker("github.com/rubykatzen/embedder:second.md"),
marker("github.com/rubykatzen/embedder@v0.1.0:second.md"),
"old",
close_marker(),
"",
Expand All @@ -193,8 +171,8 @@ def test_cache_uses_correct_asset_per_block() -> None:

results = check_blocks(blocks, registry)

assert results[0].latest_ref.render() == "github.com/rubykatzen/embedder:first.md"
assert results[1].latest_ref.render() == "github.com/rubykatzen/embedder:second.md"
assert results[0].latest_ref.render() == "github.com/rubykatzen/embedder@v0.1.0:first.md"
assert results[1].latest_ref.render() == "github.com/rubykatzen/embedder@v0.1.0:second.md"


def test_local_ref_body_refreshed_on_update(tmp_path: Path) -> None:
Expand Down