free-claude-code/scripts/ci.sh
Ali Khokhar c5239619be
Some checks are pending
Post merge / Warm dependency cache (ubuntu-latest) (push) Waiting to run
Post merge / Warm dependency cache (windows-latest) (push) Waiting to run
Post merge / Warm dependency cache (macos-26) (push) Waiting to run
Post merge / Warm dependency cache (macos-26-intel) (push) Waiting to run
Post merge / Warm dependency cache (macos-latest) (push) Waiting to run
Post merge / Publish to PyPI (push) Waiting to run
patch: Fix macOS app menu-bar launch identity (#2095)
## Why

Fixes #2092. Launching the macOS app through its shell wrapper replaces
the app executable with Python. AppKit then sees Python's bundle instead
of Free Claude Code, and the menu-bar item can have a zero-height window
even though pystray reports it visible.

## How

- Run the installed uv-managed Python runtime inside a native app
executable, preserving the app's bundle identity and main thread. Enable
UTF-8 and retain the tool environment for imports and child processes.
- Build arm64 and x86_64 launchers during release staging and include
both in the existing wheel and source distribution. Select the launcher
matching the installed Python architecture.
- Create and ad-hoc sign the app through a Python installer helper.
Validate its runtime, stage replacements, preserve foreign apps and
Desktop entries, and recognize existing FCC ownership markers during
upgrades and uninstall.
- Add native macOS coverage for bundle identity, visible menu-bar
geometry, server lifecycle, duplicate launches, child processes, and
reinstall. Add Windows and Linux source-distribution rebuild checks and
require these jobs in CI.




<!-- greptile_comment -->

<!-- greptile_summary -->

<h2><a
href="https://app.greptile.com/api/retrigger?id=81926736"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/RetriggerDark.svg?v=2"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=2"><img
alt="Retrigger"
src="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=2"
align="right"></picture></a><a
href="https://app.greptile.com/alishahryar1/github/Alishahryar1/free-claude-code/-/pull/2095"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewInGreptileDark.svg?v=2"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewInGreptile.svg?v=2"><img
alt="View in Greptile"
src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewInGreptile.svg?v=2"
align="right"></picture></a>Confidence Score: 5/5</h2>

<!-- greptile-impact-verdict --> No outstanding findings prevent
merging.

<details open><summary><h3>Summary</h3></summary>

The PR replaces the macOS shell launcher with a native executable that
keeps Free Claude Code's app identity while running its installed Python
environment.
- The macOS app keeps its identity while running the desktop app.
- Release packages carry native launchers for both Mac architectures.
- CI checks the native Mac app on Intel and Apple silicon.
</details>

<!-- greptile_confidence_score:5 -->

<sub>Reviews (2) · Last reviewed commit: ["Restore POSIX uv reuse
regression
covera..."](b50bb8449e)
· [Reviewed by
Greptile](https://www.greptile.com/?utm_source=greptile_review&utm_medium=github&utm_campaign=review_public&utm_content=alishahryar1/free-claude-code)</sub>

<!-- /greptile_comment -->
2026-10-15 18:24:22 -07:00

232 lines
5.4 KiB
Bash
Executable file

#!/bin/sh
set -eu
CHECK_ORDER="suppressions ruff-format ruff-check ty pytest playwright macos-desktop"
dry_run=0
only_checks=""
skip_checks=""
show_usage() {
cat <<'USAGE'
Usage: ci.sh [options]
Runs the local sequence for the same check IDs enforced by GitHub CI.
Requires uv on PATH when running ruff, ty, pytest, or Playwright checks.
Local ruff checks repair formatting and autofixable lint before later checks.
Checks (in order):
suppressions Ban type ignores and legacy future annotations
ruff-format uv run ruff format
ruff-check uv run ruff check --fix
ty uv run ty check
pytest uv run pytest -v --tb=short
playwright Install Chromium and run deterministic Admin browser tests
macos-desktop Build and test the native app (macOS GUI required, --only)
Options:
--only ID Run only the given check (repeatable)
--skip ID Skip the given check (repeatable)
--dry-run Print commands without running them.
--help Show this help text.
USAGE
}
fail() {
printf 'error: %s\n' "$*" >&2
exit 1
}
step() {
printf '\n==> %s\n' "$1"
}
quote_arg() {
case "$1" in
*[!A-Za-z0-9_./:@%+=,-]*|"")
escaped=$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g')
printf '"%s"' "$escaped"
;;
*)
printf '%s' "$1"
;;
esac
}
print_command() {
printf '+'
for arg in "$@"; do
printf ' '
quote_arg "$arg"
done
printf '\n'
}
run() {
print_command "$@"
if [ "$dry_run" -eq 0 ]; then
"$@"
fi
}
valid_check_id() {
case "$1" in
suppressions | ruff-format | ruff-check | ty | pytest | playwright | macos-desktop) return 0 ;;
*) return 1 ;;
esac
}
validate_check_id() {
if ! valid_check_id "$1"; then
fail "unknown check id: $1 (expected one of: $CHECK_ORDER)"
fi
}
contains_check_id() {
list=$1
id=$2
case " $list " in
*" $id "*) return 0 ;;
*) return 1 ;;
esac
}
should_run_check() {
check_id=$1
if [ "$check_id" = "macos-desktop" ] && [ -z "$only_checks" ]; then
return 1
fi
if [ -n "$only_checks" ] && ! contains_check_id "$only_checks" "$check_id"; then
return 1
fi
if contains_check_id "$skip_checks" "$check_id"; then
return 1
fi
return 0
}
assert_uv_available() {
if ! command -v uv >/dev/null 2>&1; then
fail "uv is required but was not found on PATH. Install uv first (see README or scripts/install.sh)."
fi
}
selected_checks_need_uv() {
if [ "$dry_run" -ne 0 ]; then
return 1
fi
for check_id in $CHECK_ORDER; do
if should_run_check "$check_id" && [ "$check_id" != "suppressions" ]; then
return 0
fi
done
return 1
}
run_suppressions() {
step "Ban suppressions and legacy annotations"
pattern='# type: ignore|# ty: ignore|from __future__ import annotations'
print_command grep -rE "$pattern" --include='*.py' . \
--exclude-dir=.venv --exclude-dir=.git
if [ "$dry_run" -eq 0 ]; then
if grep -rE "$pattern" --include='*.py' . \
--exclude-dir=.venv --exclude-dir=.git; then
fail "type: ignore / ty: ignore comments and legacy future annotations are not allowed. Fix the underlying type/import issue instead."
fi
fi
}
run_ruff_format() {
step "ruff format"
run uv run ruff format
}
run_ruff_check() {
step "ruff check --fix"
run uv run ruff check --fix
}
run_ty() {
step "ty check"
run uv run ty check
}
run_pytest() {
step "pytest"
run uv run pytest -v --tb=short
}
run_playwright() {
step "playwright"
run uv run playwright install chromium
run uv run pytest e2e -v --tb=short \
--tracing=retain-on-failure --screenshot=only-on-failure \
--full-page-screenshot --output=test-results
}
run_check() {
case "$1" in
suppressions) run_suppressions ;;
ruff-format) run_ruff_format ;;
ruff-check) run_ruff_check ;;
ty) run_ty ;;
pytest) run_pytest ;;
playwright) run_playwright ;;
macos-desktop)
step "macOS desktop"
run uv run --locked python scripts/build_macos_launcher.py
run uv run --locked pytest --confcutdir=smoke/desktop -n 0 -s -v smoke/desktop
;;
*) fail "unknown check id: $1" ;;
esac
}
parse_args() {
while [ "$#" -gt 0 ]; do
case "$1" in
--only)
shift
[ "$#" -gt 0 ] || fail "--only requires a check id."
validate_check_id "$1"
only_checks="${only_checks} $1"
;;
--skip)
shift
[ "$#" -gt 0 ] || fail "--skip requires a check id."
validate_check_id "$1"
skip_checks="${skip_checks} $1"
;;
--dry-run)
dry_run=1
;;
--help|-h)
show_usage
exit 0
;;
*)
show_usage >&2
fail "unknown option: $1"
;;
esac
shift
done
}
parse_args "$@"
if selected_checks_need_uv; then
assert_uv_available
fi
for check_id in $CHECK_ORDER; do
if should_run_check "$check_id"; then
run_check "$check_id"
fi
done
printf '\nAll selected CI checks passed.\n'