Skip to content
Closed
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
17 changes: 9 additions & 8 deletions .github/downstream/bump-agent-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,17 @@ jobs:
python-version: "3.12"

- name: Resolve target version
env:
TARGET_VERSION: ${{ github.event.client_payload.version || inputs.version }}
run: |
set -euo pipefail
version="${{ github.event.client_payload.version || inputs.version }}"
case "$version" in
v[0-9]*) ;;
*) echo "Refusing to pin '$version': expected a v-prefixed release tag." >&2
exit 1 ;;
esac
echo "VERSION=$version" >> "$GITHUB_ENV"
echo "BRANCH=chore/agent-core-$version" >> "$GITHUB_ENV"
# Keep event data out of shell source: workflow expressions are
# expanded before bash parses the script, so direct interpolation
# would make a crafted manual-dispatch input executable.
python3 .github/workflows/repin_agent_core.py \
--validate-only "$TARGET_VERSION"
printf 'VERSION=%s\n' "$TARGET_VERSION" >> "$GITHUB_ENV"
printf 'BRANCH=chore/agent-core-%s\n' "$TARGET_VERSION" >> "$GITHUB_ENV"

# The dependency is declared as ssh://git@github.com/...; rewrite that
# exact scheme so uv resolves it over HTTPS with a token instead of
Expand Down
31 changes: 31 additions & 0 deletions .github/downstream/repin_agent_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@
# Anchor on the repository path so the '@' inside 'git@github.com' is never
# mistaken for the rev separator.
PIN = re.compile(r'(AgentCore\.git@)[^"\'\s]+')
# AgentCore follows a three-part release scheme and may publish standard PEP 440
# prereleases, post releases, dev releases, or local versions. Keeping this an
# allow-list also makes the value safe for GITHUB_ENV and git branch names.
VERSION = re.compile(
r"v[0-9]+\.[0-9]+\.[0-9]+"
r"(?:(?:a|b|rc)[0-9]+)?"
r"(?:\.post[0-9]+)?"
r"(?:\.dev[0-9]+)?"
r"(?:\+[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?"
)


def is_valid_version(version: str) -> bool:
return VERSION.fullmatch(version) is not None


def repin(text: str, version: str) -> tuple[str, int]:
Expand All @@ -29,8 +43,25 @@ def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("version", help="Release tag to pin, e.g. v0.2.0")
parser.add_argument("--file", default="pyproject.toml")
parser.add_argument(
"--validate-only",
action="store_true",
help="Validate the release tag without editing the dependency file.",
)
args = parser.parse_args(argv)

if not is_valid_version(args.version):
print(
f"Refusing to pin {args.version!r}: expected an AgentCore release tag "
"such as v0.2.0 or v0.3.0rc1.",
file=sys.stderr,
)
return 1

if args.validate_only:
print(f"Validated release tag {args.version}.")
return 0

path = Path(args.file)
original = path.read_text(encoding="utf-8")
updated, count = repin(original, args.version)
Expand Down
75 changes: 75 additions & 0 deletions tests/test_downstream_repin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from __future__ import annotations

import importlib.util
from pathlib import Path
from types import ModuleType

import pytest


def _load_repin_module() -> ModuleType:
path = Path(__file__).parents[1] / ".github" / "downstream" / "repin_agent_core.py"
spec = importlib.util.spec_from_file_location("repin_agent_core", path)
assert spec is not None
assert spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


repin_agent_core = _load_repin_module()


@pytest.mark.parametrize(
"version",
[
"v0.2.0",
"v1.0.0rc1",
"v1.0.0.post1",
"v1.0.0.dev1",
"v1.0.0+linux.x86",
],
)
def test_accepts_release_tags(version: str) -> None:
assert repin_agent_core.is_valid_version(version)


@pytest.mark.parametrize(
"version",
[
"0.2.0",
"v1",
"v1.2",
"v1.2.3 ",
"v1.2.3/other",
"v1.2.3\nINJECTED=value",
'v1.2.3"$(echo injected)"',
],
)
def test_rejects_unsafe_or_malformed_tags(version: str) -> None:
assert not repin_agent_core.is_valid_version(version)


def test_validation_fails_before_opening_dependency_file(tmp_path: Path) -> None:
missing = tmp_path / "missing.toml"

result = repin_agent_core.main(
["v1.2.3\nINJECTED=value", "--file", str(missing)]
)

assert result == 1
assert not missing.exists()


def test_repin_replaces_exactly_one_agent_core_revision() -> None:
original = (
'dependencies = [\n'
' "apodex-agent-core @ '
'git+ssh://git@github.com/ApodexAI/AgentCore.git@abc123",\n'
']\n'
)

updated, count = repin_agent_core.repin(original, "v0.2.0")

assert count == 1
assert "AgentCore.git@v0.2.0" in updated