Skip to content

Zerm 2.8.2: rebuild Meetings and unify Speech workflows #206

Zerm 2.8.2: rebuild Meetings and unify Speech workflows

Zerm 2.8.2: rebuild Meetings and unify Speech workflows #206

Workflow file for this run

name: CI
# Runs on every push to Production and on all pull requests.
# Tests what actually matters for the current codebase:
# - Native macOS Swift app builds (Xcode on macos-latest)
# - GitHub Pages website linting
#
# Windows and Linux are not supported — Zerm is a native macOS Swift app
# built with AppKit/SwiftUI and has no cross-platform build path.
# The previous CI tested an archived Tauri prototype that is no longer shipped.
on:
push:
branches: [Production, main]
pull_request:
jobs:
# ── Swift build (macOS only) ─────────────────────────────────────────────
swift-build:
name: Swift build (macOS)
runs-on: macos-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode.app
- name: Cache pinned native frameworks
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: |
~/Zerm-Dependencies/whisper.cpp/build-apple/whisper.xcframework
~/Zerm-Dependencies/sherpa-onnx/build-swift-macos/sherpa-onnx.xcframework
~/Zerm-Dependencies/sherpa-onnx/build-swift-macos/onnxruntime.xcframework
~/Zerm-Dependencies/llama/build-apple/llama.xcframework
key: zerm-native-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Makefile') }}
- name: Build pinned native frameworks
run: |
set -euo pipefail
command -v cmake >/dev/null || brew install cmake
make setup
- name: Resolve Swift packages
run: |
set -euo pipefail
xcodebuild -project Zerm.xcodeproj \
-scheme Zerm \
-resolvePackageDependencies \
-clonedSourcePackagesDirPath .ci-packages \
2>&1 | tail -5
# Fail with a focused diagnostic before xcodebuild if a pinned upstream packaging step
# changed its output layout. A successful job must compile and test the actual app.
- name: Check native dependencies are available
run: |
set -euo pipefail
MISSING=0
for fw in \
"$HOME/Zerm-Dependencies/whisper.cpp/build-apple/whisper.xcframework" \
"$HOME/Zerm-Dependencies/sherpa-onnx/build-swift-macos/sherpa-onnx.xcframework" \
"$HOME/Zerm-Dependencies/sherpa-onnx/build-swift-macos/onnxruntime.xcframework" \
"$HOME/Zerm-Dependencies/llama/build-apple/llama.xcframework"; do
if [ ! -d "$fw" ]; then echo "missing: $fw"; MISSING=1; fi
done
if [ $MISSING -eq 1 ]; then
echo "::error::Native XCFrameworks are absent, so the app cannot be built or tested here."
echo "::error::This job previously piped xcodebuild into xcpretty/tail, which discarded the"
echo "::error::exit code and reported success for a build that failed on every run."
exit 1
fi
- name: Build (no signing)
run: |
set -euo pipefail
xcodebuild \
-project Zerm.xcodeproj \
-scheme Zerm \
-configuration Debug \
-clonedSourcePackagesDirPath .ci-packages \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=YES \
DEVELOPMENT_TEAM="" \
-quiet \
build
# The scheme has always had a TestAction wired to ZermTests, but nothing ever
# invoked it, so the suite never ran. Debug is required: `@testable import Zerm`
# needs ENABLE_TESTABILITY, which Release turns off.
- name: Unit tests
run: |
set -euo pipefail
xcodebuild test \
-project Zerm.xcodeproj \
-scheme Zerm \
-configuration Debug \
-destination 'platform=macOS' \
-only-testing:ZermTests \
-clonedSourcePackagesDirPath .ci-packages \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
-quiet
# UI automation itself needs a logged-in, unlocked Aqua session plus a user-granted
# automation permission. GitHub-hosted runners cannot provide that trustworthy runtime
# gate, but they can still prevent an unbuildable UI-test harness from reaching a PR.
- name: UI tests compile and link
run: |
set -euo pipefail
xcodebuild build-for-testing \
-project Zerm.xcodeproj \
-scheme Zerm \
-configuration Debug \
-destination 'platform=macOS' \
-only-testing:ZermUITests \
-clonedSourcePackagesDirPath .ci-packages \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
-quiet
# ── Website lint ─────────────────────────────────────────────────────────
website-lint:
name: Website lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Check HTML is valid (basic)
run: |
# Ensure index.html exists and has required landmarks
test -f docs/index.html || (echo "docs/index.html missing" && exit 1)
grep -q 'id="primary-download"' docs/index.html || (echo "Primary download button missing" && exit 1)
grep -q 'id="download"' docs/index.html || (echo "Download section anchor missing" && exit 1)
echo "HTML landmarks OK"
- name: Check site.js has no GitHub Releases redirects
run: |
# Ensure we never send users to /releases page (regression guard)
if grep -q 'github.com/arcusis/Zerm/releases"' docs/site.js; then
echo "FAIL: site.js redirects to GitHub Releases page — fix direct download logic"
exit 1
fi
echo "No GitHub Releases redirect found — OK"
- name: Check every in-app doc link has a published page
run: |
# Zerm/Resources/Links.swift is the single registry of URLs the app opens.
# Every Links.Doc slug must have a generated page under docs/docs/, or the
# app ships a link to a 404 — which is exactly how it ended up pointing at
# an unregistered domain for seventeen call sites.
REGISTRY=Zerm/Resources/Links.swift
test -f "$REGISTRY" || (echo "$REGISTRY missing" && exit 1)
SLUGS=$(sed -n 's/^ *case [A-Za-z]* = "\([a-z0-9-]*\)"$/\1/p' "$REGISTRY")
test -n "$SLUGS" || (echo "FAIL: no doc slugs parsed out of $REGISTRY" && exit 1)
MISSING=0
while IFS= read -r slug; do
if [ ! -f "docs/docs/$slug.html" ]; then
echo "FAIL: Links.Doc \"$slug\" has no page at docs/docs/$slug.html"
MISSING=1
fi
done <<< "$SLUGS"
# And the other way round, so a page never quietly stops being linked
for page in docs/docs/*.html; do
slug=$(basename "$page" .html)
[ "$slug" = "index" ] && continue
if ! echo "$SLUGS" | grep -qx "$slug"; then
echo "FAIL: docs/docs/$slug.html has no matching Links.Doc case"
MISSING=1
fi
done
[ "$MISSING" -eq 0 ] || exit 1
echo "All $(echo "$SLUGS" | wc -l | tr -d ' ') doc links resolve to a published page — OK"
- name: Check every docs screenshot exists
run: |
# The docs pages embed screenshots of the app. A missing file renders as a
# broken image on a published page, and nothing else in the pipeline notices.
MISSING=0
for page in docs/docs/*.html; do
for img in $(grep -oE 'src="img/[^"]+"' "$page" | sed 's/src="//; s/"$//'); do
if [ ! -s "docs/docs/$img" ]; then
echo "FAIL: $page references docs/docs/$img, which is missing or empty"
MISSING=1
fi
done
done
[ $MISSING -eq 0 ] || exit 1
echo "All docs screenshots resolve — OK"
- name: Check the announcements feed is published and parses
run: |
# AnnouncementsService fetches docs/announcements.json on every launch.
# An unpublished or malformed feed is a silent failure in the app, so it
# fails the PR instead. `make site` mirrors the repo-root source into docs/.
test -f docs/announcements.json || (echo "FAIL: docs/announcements.json missing — run 'make site'" && exit 1)
node -e '
const fs = require("fs");
const feed = JSON.parse(fs.readFileSync("docs/announcements.json", "utf8"));
if (!Array.isArray(feed)) throw new Error("announcements feed must be a JSON array");
const source = fs.readFileSync("announcements.json", "utf8");
if (source !== fs.readFileSync("docs/announcements.json", "utf8")) {
throw new Error("docs/announcements.json has drifted from the repo-root source — run \"make site\"");
}
console.log(`Announcements feed OK — ${feed.length} entries`);
'
- name: Check nothing points at the unregistered domain
run: |
# tryzerm.com is a find/replace artefact inherited from upstream VoiceInk's
# tryvoiceink.com. It has never been registered, so every one of these was a
# link to nowhere. Zerm/Resources/Links.swift is the replacement.
if git grep -n "tryzerm\.com" -- "*.swift" "*.json" "*.html" "*.md"; then
echo "FAIL: tryzerm.com is not a registered domain — use Links.swift"
exit 1
fi
echo "No links to the unregistered domain — OK"
# ── Security scan ────────────────────────────────────────────────────────
# Secret detection is handled by the GitGuardian app check and CodeQL, which
# run on every PR. This job only covers the repo-specific home-path check.
# (The repo's Actions policy is `selected` + sha-pinned, so third-party
# actions like gitleaks/gitleaks-action are not permitted to start.)
security-scan:
name: Secret / PII scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
fetch-depth: 0
- name: Scan for hardcoded personal paths
run: |
# Ensure no absolute home-directory paths are committed in source
FOUND=$(git grep -rE "/Users/[a-z][a-z0-9_-]+" \
-- "*.swift" "*.yml" "*.yaml" "*.md" "*.html" "*.js" "*.json" \
| grep -v "arcusis\|example\|placeholder" || true)
if [ -n "$FOUND" ]; then
echo "FAIL: Hardcoded user home paths found:"
echo "$FOUND"
exit 1
fi
echo "No hardcoded home paths — OK"
- name: Validate Sparkle appcast integrity
run: |
# The appcast XML itself is not signed — only the enclosure bytes are.
# Guard the two properties that keep OTA safe: every advertised update
# must carry an EdDSA signature and be served from the official
# GitHub Releases host. A missing/blank signature or an off-host
# enclosure fails the PR instead of shipping a bad or unverifiable feed.
for feed in docs/appcast.xml appcast.xml; do
[ -f "$feed" ] || continue
echo "Checking $feed"
enclosures=$(grep -c "<enclosure " "$feed" || true)
if [ "$enclosures" -eq 0 ]; then
echo "FAIL: $feed has no <enclosure> element"; exit 1
fi
sigs=$(grep -c 'sparkle:edSignature="[^"]\{20,\}"' "$feed" || true)
if [ "$sigs" -ne "$enclosures" ]; then
echo "FAIL: $feed has $enclosures enclosure(s) but $sigs signed — every update must be EdDSA-signed"; exit 1
fi
if grep -q '<enclosure ' "$feed" && ! grep 'url="https://github.com/arcusis/Zerm/releases/download/' "$feed" >/dev/null; then
echo "FAIL: $feed enclosure is not served from the official GitHub Releases host"; exit 1
fi
done
echo "Appcast integrity — OK"