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
13 changes: 12 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,17 @@ jobs:
print(f"OK: {whl} ships only entry.js under ui-tui/, no node_modules")
PY

# 4b) Export the locked dependency set as a constraints file. The install
# scripts pass this to `uv tool install -c` so a one-click install gets
# the exact versions we lock and test, instead of re-resolving to the
# newest versions allowed by pyproject (which drifts, and can pull a
# source-only build with no wheel on the user's platform). --locked
# fails the release if uv.lock is stale vs pyproject, so we never ship
# constraints that conflict with the wheel's own metadata; --all-extras
# covers the channel SDKs behind raven[channels].
- name: Export locked constraints
run: uv export --locked --all-extras --no-hashes --no-emit-project -o dist/raven-constraints.txt

- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
Expand Down Expand Up @@ -121,7 +132,7 @@ jobs:
id=""
fi
if [ -z "$id" ]; then
gh release create "$tag" dist/*.whl dist/*.tar.gz \
gh release create "$tag" dist/*.whl dist/*.tar.gz dist/raven-constraints.txt \
--title "Raven $ver ($today)" --notes-file "$RUNNER_TEMP/notes.md" --draft $pre
else
echo "Release $tag already published (id=$id); leaving as-is."
Expand Down
17 changes: 13 additions & 4 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ Highlights before publishing. Structure:
- Version: `X.Y.Z`
- Tag: `vX.Y.Z`
- Stability: <public preview patch | public preview minor | ...> # fill by hand per release type
- Assets: wheel and source distribution attached to this release
- Assets: wheel, source distribution, and locked constraints
(`raven-constraints.txt`) attached to this release

## Notes
- pre-1.0 evolution caveat
Expand All @@ -49,10 +50,12 @@ Highlights before publishing. Structure:

## Flow

1. Bump `version` in `pyproject.toml`; open a PR; merge to `main`.
1. Bump `version` in `pyproject.toml`, run `uv lock`, then open a PR; merge to `main`.
2. `git tag vX.Y.Z && git push origin vX.Y.Z`.
3. CI (`release.yml`) builds the wheel + sdist and creates a **draft** GitHub
Release with both attached, titled and prefilled from the template.
3. CI (`release.yml`) builds the wheel + sdist, exports the locked constraints
from `uv.lock` (`uv export --locked`, which fails the release if the lock is
stale vs pyproject), and creates a **draft** GitHub Release with all three
attached, titled and prefilled from the template.
4. Fill the summary + Highlights in the draft, then click **Publish**.
Publishing makes it `/releases/latest`, which `install.sh` serves.

Expand All @@ -69,3 +72,9 @@ Highlights before publishing. Structure:
draft: publishing is a deliberate human step.
- PyPI publishing is not wired up; the supported install path is the GitHub
Release wheel asset resolved by `install.sh`.
- `install.sh` / `install.ps1` pass `raven-constraints.txt` to
`uv tool install -c`, so a one-click install gets the exact locked versions we
test rather than re-resolving to the newest allowed by `pyproject.toml`. A
release older than this asset installs without pinning (the scripts degrade
gracefully). To lift a pin later, update `uv.lock` and cut a new release; the
install scripts need no change.
39 changes: 35 additions & 4 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,27 @@ function Resolve-RavenWheel {
return $asset.browser_download_url
}

function Resolve-RavenConstraints([string]$WheelUrl) {
# Derive the locked-constraints URL from the wheel URL (same release dir) so
# the constraints always match the wheel being installed -- including when
# RAVEN_WHEEL_URL pins an older wheel. Returns a local temp-file path, or
# $null when the asset is absent (release predates it) or the download fails,
# so the installer degrades to an unconstrained install rather than failing.
$url = $env:RAVEN_CONSTRAINTS_URL
if (-not $url) {
if ($WheelUrl -notmatch "/[^/]+\.whl$") { return $null }
$url = $WheelUrl -replace "/[^/]+\.whl$", "/raven-constraints.txt"
}
$dest = Join-Path ([IO.Path]::GetTempPath()) ("raven-constraints-" + [guid]::NewGuid().ToString("N") + ".txt")
try {
Invoke-WebRequest $url -OutFile $dest
} catch {
Write-Warn "Could not download locked constraints; installing without version pinning."
return $null
}
return $dest
}

function Install-Raven([string]$UvPath, [string]$NodePath) {
$scriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
$pyproject = Join-Path $scriptDir "pyproject.toml"
Expand All @@ -225,26 +246,36 @@ function Install-Raven([string]$UvPath, [string]$NodePath) {
Write-Warn "Found node but not npm; skipping TUI bundle build"
}
}
# Pin to the locked dependency set so an install matches what we test.
$constraints = Join-Path ([IO.Path]::GetTempPath()) ("raven-constraints-" + [guid]::NewGuid().ToString("N") + ".txt")
& $UvPath export --directory "$scriptDir" --frozen --all-extras --no-hashes --no-emit-project -o "$constraints"
# Install all channel adapters by default; fall back to base raven if
# the umbrella extra fails to build on this platform, so one broken
# channel SDK cannot block the whole install.
try {
& $UvPath tool install --force -e "$scriptDir[channels]"
& $UvPath tool install --force -c "$constraints" -e "$scriptDir[channels]"
if ($LASTEXITCODE -ne 0) { throw "channel extras install failed" }
} catch {
Write-Warn "Channel dependencies failed to install; installed base raven only. Some channels stay unavailable (see: raven channels list)."
& $UvPath tool install --force -e "$scriptDir"
& $UvPath tool install --force -c "$constraints" -e "$scriptDir"
if ($LASTEXITCODE -ne 0) { Fail "Raven install failed." }
}
} else {
$wheelUrl = Resolve-RavenWheel
$constraints = Resolve-RavenConstraints $wheelUrl
if ($constraints) {
$cArgs = @("-c", $constraints)
} else {
Write-Warn "Release has no locked-constraints asset; installing without version pinning."
$cArgs = @()
}
Write-Info " installing $wheelUrl"
try {
& $UvPath tool install --force "raven[channels] @ $wheelUrl"
& $UvPath tool install --force @cArgs "raven[channels] @ $wheelUrl"
if ($LASTEXITCODE -ne 0) { throw "channel extras install failed" }
} catch {
Write-Warn "Channel dependencies failed to install; installed base raven only. Some channels stay unavailable (see: raven channels list)."
& $UvPath tool install --force $wheelUrl
& $UvPath tool install --force @cArgs $wheelUrl
if ($LASTEXITCODE -ne 0) { Fail "Raven install failed." }
}
}
Expand Down
31 changes: 27 additions & 4 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,15 @@ install_raven() {
warn "No usable node found; skipping TUI build; raven tui may not work"
fi
fi
# Pin to the locked dependency set so an install matches what we test.
constraints="$(mktemp)"
uv export --directory "$script_dir" --frozen --all-extras --no-hashes --no-emit-project -o "$constraints"
# Install all channel adapters by default. If the umbrella extra fails to
# resolve/build on this platform, fall back to base raven so one broken
# channel SDK cannot block the whole install.
if ! uv tool install --force -e "$script_dir[channels]"; then
if ! uv tool install --force -c "$constraints" -e "$script_dir[channels]"; then
warn "Channel dependencies failed to install; installed base raven only. Some channels stay unavailable (see: raven channels list)."
uv tool install --force -e "$script_dir"
uv tool install --force -c "$constraints" -e "$script_dir"
fi
else
# Remote mode: install the latest published release wheel, which bundles
Expand All @@ -190,10 +193,30 @@ install_raven() {
| grep -oE 'https://[^"]*/raven-[^"]*\.whl' | head -n1)"
fi
[ -n "$wheel_url" ] || die "Could not resolve the latest raven release wheel from GitHub (check network, or set RAVEN_WHEEL_URL to a wheel URL)."
# Derive the locked-constraints URL from the wheel URL (same release dir) so
# the constraints always match the wheel being installed, including when
# RAVEN_WHEEL_URL pins an older wheel. Missing asset / download failure ->
# install without pinning rather than fail.
constraints_url="${RAVEN_CONSTRAINTS_URL:-}"
if [ -z "$constraints_url" ]; then
case "$wheel_url" in
*.whl) constraints_url="${wheel_url%/*}/raven-constraints.txt" ;;
esac
fi
c_args=""
if [ -n "$constraints_url" ]; then
constraints="$(mktemp)"
if curl -fsSL "$constraints_url" -o "$constraints" 2>/dev/null; then
c_args="-c $constraints"
else
warn "Could not download locked constraints; installing without version pinning."
fi
fi
info " installing $wheel_url"
if ! uv tool install --force "raven[channels] @ $wheel_url"; then
# shellcheck disable=SC2086 # $c_args is an intentional word-split option pair.
if ! uv tool install --force $c_args "raven[channels] @ $wheel_url"; then
warn "Channel dependencies failed to install; installed base raven only. Some channels stay unavailable (see: raven channels list)."
uv tool install --force "$wheel_url"
uv tool install --force $c_args "$wheel_url"
fi
fi
# Ensure ~/.local/bin (uv tool bin dir) is on PATH for future shells.
Expand Down
33 changes: 29 additions & 4 deletions raven/cli/upgrade_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,36 @@ def main(argv=None):
if parent_status != 0:
return parent_status

# Pin to the same locked constraints the installer uses. Derive the URL from
# the (already trust-checked) wheel URL so the constraints always match the
# wheel being installed. Missing asset / download failure -> upgrade without
# pinning rather than abort.
constraints_path = None
constraints_url = wheel_url.rsplit("/", 1)[0] + "/raven-constraints.txt"
try:
import os
import socket
import tempfile
import urllib.request

socket.setdefaulttimeout(30)
fd, constraints_path = tempfile.mkstemp(prefix="raven-constraints-", suffix=".txt")
os.close(fd)
urllib.request.urlretrieve(constraints_url, constraints_path)
except Exception:
print(
"Warning: could not download locked constraints; "
"upgrading without version pinning.",
file=sys.stderr,
)
constraints_path = None

def install(requirement):
return subprocess.run(
[uv_path, "tool", "install", "--force", requirement],
check=False,
).returncode
command = [uv_path, "tool", "install", "--force"]
if constraints_path:
command += ["-c", constraints_path]
command.append(requirement)
return subprocess.run(command, check=False).returncode

try:
channel_status = install(f"raven[channels] @ {wheel_url}")
Expand Down
67 changes: 67 additions & 0 deletions tests/test_cli_upgrade_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,17 @@ def test_uv_tool_target_rejects_malformed_target_fields(
upgrade_commands._uv_tool_target()


@pytest.fixture(autouse=True)
def _stub_constraints_urlretrieve(monkeypatch: pytest.MonkeyPatch) -> None:
# The upgrade helper downloads locked constraints via urllib before running
# uv. Stub it so helper tests never touch the network; default to failure so
# the graceful no-pin path (the pre-constraints command shape) is what the
# existing assertions observe. Tests that exercise pinning override this.
import urllib.request

monkeypatch.setattr(urllib.request, "urlretrieve", Mock(side_effect=OSError("no network")))


def _load_upgrade_helper() -> object:
namespace: dict[str, object] = {"__name__": "raven_upgrade_helper_test"}
exec(upgrade_commands._UPGRADE_HELPER_SOURCE, namespace)
Expand Down Expand Up @@ -456,6 +467,62 @@ def test_upgrade_helper_catches_uv_execution_errors(
assert "access denied" in capsys.readouterr().err


def test_upgrade_helper_pins_constraints_when_download_succeeds(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import urllib.request

downloaded: list[tuple[str, str]] = []

def fake_urlretrieve(url: str, filename: str) -> tuple[str, None]:
downloaded.append((url, filename))
return filename, None

monkeypatch.setattr(urllib.request, "urlretrieve", fake_urlretrieve)
run = Mock(return_value=Mock(returncode=0))
monkeypatch.setattr(subprocess, "run", run)
helper_main = _load_upgrade_helper()

status = helper_main(["/usr/bin/uv", WHEEL_URL, "0.1.3", "0.1.4"])

assert status == 0
assert len(downloaded) == 1
constraints_url, constraints_path = downloaded[0]
assert constraints_url == ("https://github.com/EverMind-AI/Raven/releases/download/v0.1.4/raven-constraints.txt")
run.assert_called_once_with(
[
"/usr/bin/uv",
"tool",
"install",
"--force",
"-c",
constraints_path,
f"raven[channels] @ {WHEEL_URL}",
],
check=False,
)


def test_upgrade_helper_skips_constraints_when_download_fails(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
# The autouse stub makes the constraints download fail; the helper must fall
# back to an unpinned install rather than abort.
run = Mock(return_value=Mock(returncode=0))
monkeypatch.setattr(subprocess, "run", run)
helper_main = _load_upgrade_helper()

status = helper_main(["/usr/bin/uv", WHEEL_URL, "0.1.3", "0.1.4"])

assert status == 0
run.assert_called_once_with(
["/usr/bin/uv", "tool", "install", "--force", f"raven[channels] @ {WHEEL_URL}"],
check=False,
)
assert "upgrading without version pinning" in capsys.readouterr().err


def test_upgrade_helper_waits_for_parent_before_running_uv(
monkeypatch: pytest.MonkeyPatch,
) -> None:
Expand Down
Loading