-
Notifications
You must be signed in to change notification settings - Fork 0
356 lines (325 loc) · 16.3 KB
/
Copy pathci.yml
File metadata and controls
356 lines (325 loc) · 16.3 KB
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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
name: CI
on:
pull_request:
branches: [main]
types: [opened, reopened, ready_for_review, synchronize]
merge_group: # Merge-queue runs: validate the queued merge result so two
# PRs that are each green against an older main can't land a
# semantic conflict together.
push:
branches: [main] # PRs are covered by pull_request (incl. synchronize);
# scoping push to main avoids double-running every PR commit.
# Least privilege: CI only needs to read the repo. Actions are pinned to commit
# SHAs (a moved tag can't silently change what runs); Dependabot keeps them current.
permissions:
contents: read
# Cancel superseded runs when new commits land on a PR/branch, but never cancel a
# main run (don't kill an in-flight merge build).
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
check:
name: lint + typecheck + tests (py${{ matrix.python-version }})
runs-on: ubuntu-latest
timeout-minutes: 20
# Test both ends of the supported range: 3.12 is the floor (requires-python),
# 3.13 is what the Homebrew formula ships. fail-fast off so one version's
# failure doesn't mask the other's.
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.13"]
# Pin the interpreter every `uv run`/`uv build` in check.sh resolves to, so the
# matrix actually exercises each version rather than whatever uv would pick.
env:
UV_PYTHON: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false # no job pushes; don't leave the token in .git/config
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}
cache: pip
# PortAudio backs sounddevice; ffmpeg decodes non-WAV/URL audio (the `--sample`
# stream tests build a FileSource for the hosted sample, which needs ffmpeg).
# Slow-mirror resilience (bounded retry + trimmed payload) lives in the script.
- name: System deps (PortAudio + ffmpeg)
run: ./scripts/ci_install_audio_deps.sh
# check.sh lints Markdown and template JS/CSS via Node CLIs; versions are
# pinned in scripts/gate_tool_pins.sh (shared with the web session-start
# hook). The runner ships Node, so a global npm install suffices.
- name: Node lint CLIs
run: |
source scripts/gate_tool_pins.sh
npm install -g "markdownlint-cli@${MARKDOWNLINT_VERSION}" "prettier@${PRETTIER_VERSION}"
# check.sh runs every tool through `uv run` / `uv build` for a locked,
# reproducible env, so only uv must be on PATH. setup-uv caches the uv
# download cache (~/.cache/uv) keyed on uv.lock, so the locked env — incl.
# the Rust-backed sdists (pydantic-core/jiter/cryptography) — isn't
# re-downloaded/rebuilt every run. `uv run` itself syncs the project + dev
# group into .venv, so no `pip install -e .` is needed here.
- name: Install uv (cached)
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
cache-dependency-glob: uv.lock
# actionlint and gitleaks are Go binaries (no PyPI wheel), so check.sh self-skips
# them locally like shellcheck. Build them here with the runner's preinstalled Go,
# pinned via scripts/gate_tool_pins.sh (shared with the web session-start hook),
# and put GOPATH/bin on PATH so check.sh enforces them.
# (gitleaks v8's Go module path is still github.com/zricethezav/gitleaks/v8.)
# Cache the built binaries keyed on the pin file so a cache hit skips the
# from-source `go install` compile entirely.
- name: Cache Go gate binaries (actionlint, gitleaks)
id: cache-go-bin
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/go/bin
key: go-gate-bins-${{ runner.os }}-${{ hashFiles('scripts/gate_tool_pins.sh') }}
- name: Workflow + secret scanners (actionlint, gitleaks)
env:
# Map the cache-hit output to an env var rather than expanding the
# `${{ }}` directly into the script (zizmor template-injection rule).
CACHE_HIT: ${{ steps.cache-go-bin.outputs.cache-hit }}
run: |
source scripts/gate_tool_pins.sh
if [ "$CACHE_HIT" != "true" ]; then
go install "$ACTIONLINT_MODULE"
go install "$GITLEAKS_MODULE"
fi
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
- name: Lint, typecheck, test
run: ./scripts/check.sh
# Branch protection requires a check literally named "lint + typecheck + tests",
# but `check` is a matrix, so its contexts are suffixed "(py3.12)" / "(py3.13)".
# Re-publish the un-suffixed name here, green only when every matrix cell passed
# (if: always() + an explicit result check, so a failed/skipped/cancelled matrix
# can't satisfy the required check). Point branch protection at this one stable
# name and matrix changes never break the required check again.
check-result:
name: lint + typecheck + tests
needs: [check]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Require every py-version matrix cell to have passed
run: |
if [ "${{ needs.check.result }}" != "success" ]; then
echo "check matrix result: ${{ needs.check.result }}"
exit 1
fi
echo "all py-version matrix cells passed"
windows:
name: tests (windows, py${{ matrix.python-version }})
runs-on: windows-latest
timeout-minutes: 20
# Windows can't run scripts/check.sh (it's bash plus Go/Homebrew/shell tooling), so
# this job runs only the pytest suite — enough to catch Windows-specific regressions
# (path handling, subprocess/encoding, POSIX-only assumptions). The lint/type/security
# gates stay on the Linux `check` job. Same Python ends as that matrix: 3.12 floor,
# 3.13 shipped; fail-fast off so one version's failure doesn't mask the other's.
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.13"]
# Pin the interpreter every `uv run` resolves to, so the matrix exercises each version.
env:
UV_PYTHON: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false # no job pushes; don't leave the token in .git/config
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}
cache: pip
# ffmpeg must be on PATH: the `stream --sample`/`clip`/`caption` paths probe for it
# (require_ffmpeg) before doing their work, so without it those tests fail at the
# probe rather than exercising the mocked run. PortAudio needs no install — the
# sounddevice wheel bundles it on Windows. choco ships on the runner but its download
# from community.chocolatey.org doesn't just flake — it sometimes *hangs* for the whole
# job timeout, and a plain retry loop never gets to retry because the stuck attempt
# never returns. So bound each attempt with a hard timeout (Start-Job + Wait-Job): a
# hung download is killed and the next attempt retries, instead of wedging the cell
# until it's cancelled. The shim lands in choco's bin dir (machine-wide, already on the
# runner PATH), so the parent shell and later steps pick it up.
#
# During a sustained community.chocolatey.org outage the feed returns 503s *quickly*,
# so every bounded attempt fails fast and the retry loop exhausts with no ffmpeg. Fall
# back to a static build off GitHub's release CDN (a different, far more reliable origin)
# and prepend its dir to GITHUB_PATH so later steps see it.
- name: System deps (ffmpeg)
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$env:PATH = "C:\ProgramData\chocolatey\bin;$env:PATH"
for ($i = 1; $i -le 3; $i++) {
$job = Start-Job { choco install ffmpeg --no-progress -y }
if (Wait-Job $job -Timeout 240) { Receive-Job $job } else {
Stop-Job $job
Write-Host "choco install ffmpeg hung (attempt $i); killing and retrying…"
}
Remove-Job $job -Force
if (Get-Command ffmpeg -ErrorAction SilentlyContinue) { break }
Start-Sleep -Seconds 5
}
if (-not (Get-Command ffmpeg -ErrorAction SilentlyContinue)) {
Write-Host "choco couldn't provide ffmpeg; downloading a static build from GitHub…"
$url = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip"
$zip = Join-Path $env:RUNNER_TEMP "ffmpeg.zip"
$dest = Join-Path $env:RUNNER_TEMP "ffmpeg"
Invoke-WebRequest -Uri $url -OutFile $zip
Expand-Archive -Path $zip -DestinationPath $dest -Force
$bin = (Get-ChildItem -Path $dest -Recurse -Filter ffmpeg.exe | Select-Object -First 1).DirectoryName
$env:PATH = "$bin;$env:PATH"
Add-Content -Path $env:GITHUB_PATH -Value $bin
}
ffmpeg -version
- name: Install uv (cached)
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
cache-dependency-glob: uv.lock
# `uv run` syncs the locked project + dev group into .venv, then runs the default
# suite (e2e/install excluded via addopts).
- name: Run test suite
run: uv run pytest -q
# Stable, un-suffixed name for branch protection, mirroring `check-result`: green only
# when every Windows matrix cell passed (a failed/skipped/cancelled matrix can't satisfy
# it). Point branch protection at this one name and matrix changes won't break it.
windows-result:
name: tests (windows)
needs: [windows]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Require every Windows matrix cell to have passed
run: |
if [ "${{ needs.windows.result }}" != "success" ]; then
echo "windows matrix result: ${{ needs.windows.result }}"
exit 1
fi
echo "all windows matrix cells passed"
lint-formula:
name: brew style (Homebrew formula)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false # no job pushes; don't leave the token in .git/config
# Homebrew's formula linters live inside `brew`, so set it up on the runner.
# Homebrew/actions is a monorepo (setup-homebrew is a subpath); pin it to a
# commit SHA like every other action here — Dependabot keeps it current.
- uses: Homebrew/actions/setup-homebrew@2ebcf16054461267868620b1414507f3ccc765c1
# `brew style` is the RuboCop-based formula linter and runs fully offline, so
# it lints idioms (resource/depends_on ordering, on_linux scoping) on every PR.
# A real `brew install --build-bottle` of the formula runs in release.yml when
# a tag is cut; doing it per-PR was dropped — the from-source macOS build
# (rust + cryptography) took too long for PR feedback. The stricter
# `brew audit --strict --online` still belongs in a release job.
- name: Lint the formula
run: brew style ./Formula/assembly.rb
pre-commit:
name: pre-commit
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false # no job pushes; don't leave the token in .git/config
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
cache: pip
# PortAudio backs sounddevice; ffmpeg decodes the `--sample` stream source.
- name: System deps (PortAudio + ffmpeg)
run: ./scripts/ci_install_audio_deps.sh
# The local pytest hook runs `uv run --frozen python -m pytest`, so the tests
# resolve the LOCKED dependency versions (uv.lock) rather than the newest
# release `pip install` would pull — which is what keeps the byte-exact
# `--help` snapshots stable. Install uv and materialize the frozen env here.
- name: Install uv (cached)
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
cache-dependency-glob: uv.lock
- name: Sync frozen env
run: uv sync --frozen
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
build:
name: build + twine check
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false # no job pushes; don't leave the token in .git/config
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
cache: pip
- name: Build wheel + sdist
run: |
python -m pip install build twine
python -m build
- name: Validate metadata
run: twine check dist/*
audit:
name: pip-audit (dependency CVEs)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false # no job pushes; don't leave the token in .git/config
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
cache: pip
- name: Audit runtime dependencies for known CVEs
run: |
# Keep build tooling current first: pip-audit scans the whole environment,
# so a pip/setuptools advisory that a one-line upgrade fixes would otherwise
# fail the gate on something that isn't one of our runtime dependencies.
python -m pip install --upgrade pip setuptools
python -m pip install -e . pip-audit
# Append `--ignore-vuln <ID>` to accept an unfixable transitive advisory.
python -m pip_audit
# End-to-end check that install.sh actually installs a working `assembly`. Runs
# the script in dev mode (--install-method git) so it installs *this* checkout
# editable via uv — exercising both the installer and the PR's own code — then
# smoke-tests the resulting CLI. Catches install.sh regressions (arg parsing,
# the uv/pipx selection, the editable path) that shellcheck alone can't.
install-script:
name: install script smoke
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false # no job pushes; don't leave the token in .git/config
fetch-depth: 0 # hatch-vcs derives the version from git history for the editable build
# Provide uv so install.sh takes its preferred (uv) path rather than
# bootstrapping it over the network.
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
cache-dependency-glob: uv.lock
# PortAudio + ffmpeg so `assembly --help` (which imports the full command
# tree) loads cleanly; also lets install.sh's dep check find them present.
- name: System deps (PortAudio + ffmpeg)
run: ./scripts/ci_install_audio_deps.sh
- name: Run install.sh (editable, from this checkout)
run: ./install.sh --install-method git
- name: Smoke-test the installed CLI
run: |
# uv tool installs land in ~/.local/bin; put it on PATH for this step.
export PATH="$HOME/.local/bin:$PATH"
assembly --version
help_out="$(assembly --help)"
echo "$help_out" | grep -q transcribe