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
8 changes: 8 additions & 0 deletions .github/actions/ci-environment/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ inputs:
lookup-only:
description: Check dependency cache availability without downloading it
default: "false"
sync-dependencies:
description: Install the locked project and development dependencies after cache restoration.
default: "true"
prepare-node:
description: Prepare the Node runtime used by pytest subprocess contracts.
default: "false"
Expand Down Expand Up @@ -142,3 +145,8 @@ runs:
key: ${{ steps.identity.outputs.dependencies-prefix }}${{ steps.dependencies.outputs.hash }}
restore-keys: ${{ steps.identity.outputs.dependencies-prefix }}
lookup-only: ${{ inputs.lookup-only }}

- name: Install dependencies
if: inputs.sync-dependencies == 'true'
shell: bash
run: uv sync --locked --group dev
72 changes: 0 additions & 72 deletions .github/workflows/dependency-cache.yml

This file was deleted.

142 changes: 142 additions & 0 deletions .github/workflows/post-merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
name: Post merge

env:
CI_UV_VERSION: "0.12.13"
UV_MALWARE_CHECK: "1"
UV_PREVIEW_FEATURES: "malware-check"

on:
workflow_dispatch:
push:
branches: [main]

concurrency:
group: post-merge-main
cancel-in-progress: false
queue: max

jobs:
warm-dependency-cache:
name: Warm dependency cache (${{ matrix.os }})
if: github.ref == 'refs/heads/main'
runs-on: ${{ matrix.runner }}
timeout-minutes: 15
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- os: Linux
runner: ubuntu-latest
- os: Windows
runner: windows-latest
- os: macOS
runner: macos-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ github.sha }}
fetch-depth: 1

- name: Set up CI environment
id: environment
uses: ./.github/actions/ci-environment
with:
uv-version: ${{ env.CI_UV_VERSION }}
runner-label: ${{ matrix.runner }}
lookup-only: "true"
sync-dependencies: "false"

- name: Save Python
if: steps.environment.outputs.python-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
with:
path: ${{ steps.environment.outputs.python-path }}
key: ${{ steps.environment.outputs.python-key }}

- name: Restore compatible dependency cache
if: steps.environment.outputs.dependencies-hit != 'true'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
with:
path: ${{ steps.environment.outputs.dependencies-path }}
key: ${{ steps.environment.outputs.dependencies-key }}
restore-keys: ${{ steps.environment.outputs.dependencies-prefix }}

- name: Warm dependency cache
if: steps.environment.outputs.dependencies-hit != 'true'
run: uv sync --locked --group dev --no-install-workspace

- name: Save dependency cache
if: steps.environment.outputs.dependencies-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
with:
path: ${{ steps.environment.outputs.dependencies-path }}
key: ${{ steps.environment.outputs.dependencies-key }}

publish:
name: Publish to PyPI
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 15
environment: pypi
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ github.sha }}
fetch-depth: 2

- name: Fetch previous revision
env:
BEFORE_SHA: ${{ github.event.before }}
run: |
if [ -n "$BEFORE_SHA" ]; then
git fetch --no-tags --depth=1 origin "$BEFORE_SHA"
fi

- name: Set up CI environment
uses: ./.github/actions/ci-environment
with:
uv-version: ${{ env.CI_UV_VERSION }}
runner-label: ubuntu-latest
lookup-only: "true"
sync-dependencies: "false"

- name: Detect release
id: release
env:
BEFORE_SHA: ${{ github.event.before }}
HEAD_SHA: ${{ github.sha }}
run: |
uv run --no-project --python "$(cat .python-version)" python - <<'PY'
import os
import subprocess
import tomllib
from pathlib import Path

head = os.environ["HEAD_SHA"]
before = os.environ.get("BEFORE_SHA") or f"{head}^"
def version(revision):
result = subprocess.run(
["git", "show", f"{revision}:pyproject.toml"],
check=True, capture_output=True, text=True,
)
return tomllib.loads(result.stdout)["project"]["version"]
current, previous = version(head), version(before)
with Path(os.environ["GITHUB_OUTPUT"]).open("a", encoding="utf-8") as output:
output.write(f"changed={str(current != previous).lower()}\n")
print(f"Release version: {previous} -> {current}")
PY

- name: Build and validate release
if: steps.release.outputs.changed == 'true'
run: |
uv build --no-sources --out-dir "$RUNNER_TEMP/fcc-release"
uv run --no-project --python "$(cat .python-version)" python scripts/validate_release.py "$RUNNER_TEMP/fcc-release"

- name: Publish release
if: steps.release.outputs.changed == 'true'
run: uv publish --trusted-publishing always "$RUNNER_TEMP/fcc-release/"*
41 changes: 32 additions & 9 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Branch protection: require every status check this workflow reports, for example:
# Ban suppressions and legacy annotations
# ruff-format, ruff-check, ty
# Version policy
# pytest (Linux), pytest (Windows), pytest (macOS)
# playwright (Linux), playwright (Windows), playwright (macOS)
# GitHub may prefix with the workflow name (e.g. "CI / ruff-format"); use the names the branch protection UI offers after a run.
Expand All @@ -20,6 +21,37 @@ concurrency:
cancel-in-progress: true

jobs:
version-policy:
name: Version policy
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0

- name: Fetch target revision
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
BASE_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name }}
run: git fetch --no-tags "https://github.com/$BASE_REPOSITORY.git" "$BASE_SHA"

- name: Set up CI environment
uses: ./.github/actions/ci-environment
with:
uv-version: ${{ env.CI_UV_VERSION }}
runner-label: ubuntu-latest

- name: Check version policy
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: uv run --locked --group dev python scripts/check_version_policy.py --base "$BASE_SHA" --head "$HEAD_SHA"

no-suppressions-or-legacy-annotations:
name: Ban suppressions and legacy annotations
runs-on: ubuntu-latest
Expand Down Expand Up @@ -70,9 +102,6 @@ jobs:
uv-version: ${{ env.CI_UV_VERSION }}
runner-label: ubuntu-latest

- name: Install dependencies
run: uv sync --locked --group dev

- name: Run
run: ${{ matrix.run }}

Expand Down Expand Up @@ -106,9 +135,6 @@ jobs:
runner-label: ${{ matrix.runner }}
prepare-node: "true"

- name: Install dependencies
run: uv sync --locked --group dev

- name: Run pytest
run: uv run --locked pytest -v --tb=short --durations=50

Expand Down Expand Up @@ -141,9 +167,6 @@ jobs:
uv-version: ${{ env.CI_UV_VERSION }}
runner-label: ${{ matrix.runner }}

- name: Install dependencies
run: uv sync --locked --group dev

- name: Install Chromium
run: uv run --locked playwright install ${{ matrix.os == 'Linux' && '--with-deps chromium' || 'chromium' }}

Expand Down
11 changes: 9 additions & 2 deletions e2e/test_admin_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,14 @@ def test_delayed_startup_status_does_not_replace_a_manual_provider_check(
startup: list[Route] = []
manual: list[Route] = []

page.route("**/admin/api/status", lambda route: startup.append(route))
def hold_first_status(route: Route) -> None:
# Only the stale snapshot is held. Later refreshes must finish normally.
if startup:
route.continue_()
else:
startup.append(route)

page.route("**/admin/api/status", hold_first_status)
page.route(
"**/admin/api/providers/open_router/test", lambda route: manual.append(route)
)
Expand Down Expand Up @@ -195,7 +202,7 @@ def test_delayed_startup_status_does_not_replace_a_manual_provider_check(
result = page.locator('[data-provider-check-result="open_router"]')
expect(result).to_have_text(expected)
with page.expect_response("**/admin/api/status") as response:
startup.pop(0).fulfill(json=snapshot)
startup[0].fulfill(json=snapshot)
response.value.finished()
page.wait_for_function("!state.startupRequest")
expect(result).to_have_text(expected)
Expand Down
7 changes: 5 additions & 2 deletions e2e/test_connected_accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,14 +163,17 @@ def _open(page: Page, admin_base_url: str) -> None:
def test_account_modes_wait_for_status_and_recover_after_load_failure(
page: Page, admin_base_url: str, accounts: _Accounts
) -> None:
page.expose_function(
"pendingAccountStatusCount", lambda: len(accounts.pending_status)
)
accounts.hold_status.add("github_copilot")
page.goto(f"{admin_base_url}/admin")
copilot = page.locator('[data-provider="github_copilot"]')
openai = page.locator('[data-provider="openai"]')
expect(copilot.get_by_role("button", name="Loading…", exact=True)).to_be_disabled()
expect(copilot.get_by_role("button", name="Connect", exact=True)).to_have_count(0)
expect(openai.get_by_role("button", name="Connect", exact=True)).to_be_enabled()
assert len(accounts.pending_status) == 1
page.wait_for_function("async () => await window.pendingAccountStatusCount() === 1")

accounts.pending_status.pop().fulfill(
status=503, json={"detail": "Account status unavailable."}
Expand All @@ -181,7 +184,7 @@ def test_account_modes_wait_for_status_and_recover_after_load_failure(
expect(copilot.get_by_role("button", name="Connect", exact=True)).to_have_count(0)
copilot.get_by_role("button", name="Retry", exact=True).click()
expect(copilot.get_by_role("button", name="Loading…", exact=True)).to_be_disabled()
assert len(accounts.pending_status) == 1
page.wait_for_function("async () => await window.pendingAccountStatusCount() === 1")
accounts.hold_status.clear()
accounts.pending_status.pop().fulfill(json=accounts.statuses["github_copilot"])

Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "free-claude-code"
version = "6.2.63"
version = "6.2.64"
description = "Local proxy connecting coding agents to OpenAI-compatible AI providers"
readme = "README.md"
license = "AGPL-3.0-only"
Expand Down Expand Up @@ -83,6 +83,7 @@ explicit = true

[dependency-groups]
dev = [
"packaging>=26.0",
"pytest>=9.1.1",
"pytest-asyncio>=1.4.0",
"pytest-cov>=7.1.0",
Expand Down
Loading
Loading