File size: 5,251 Bytes
081e962
 
 
9b71fd0
081e962
 
 
 
 
 
 
 
 
02356b1
081e962
02356b1
081e962
 
d7e53a2
02356b1
 
081e962
9b71fd0
081e962
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9b71fd0
081e962
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d7e53a2
 
 
081e962
 
d7e53a2
081e962
d7e53a2
081e962
 
 
 
 
02356b1
 
081e962
 
 
02356b1
 
081e962
 
 
 
 
 
 
9b71fd0
 
 
 
 
 
 
 
 
 
 
081e962
 
 
 
 
 
 
 
 
 
 
9b71fd0
081e962
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
#!/bin/sh
set -eu

CHECK_ORDER="suppressions ruff-format ruff-check ty frontend pytest"

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, or pytest 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
  frontend       Build the admin UI and verify committed assets are current
  pytest         uv run pytest -v --tb=short

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 | frontend | pytest) 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 [ -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_frontend() {
    step "frontend (admin UI build)"
    if ! command -v npm >/dev/null 2>&1; then
        fail "npm is required for the frontend check but was not found on PATH."
    fi
    run npm --prefix frontend ci
    run npm --prefix frontend run build
    step "frontend (verify committed admin assets are current)"
    run git diff --exit-code -- src/free_claude_code/api/admin_static/
}

run_pytest() {
    step "pytest"
    run uv run pytest -v --tb=short
}

run_check() {
    case "$1" in
        suppressions) run_suppressions ;;
        ruff-format) run_ruff_format ;;
        ruff-check) run_ruff_check ;;
        ty) run_ty ;;
        frontend) run_frontend ;;
        pytest) run_pytest ;;
        *) 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'