Publish the 2.7.1 appcast entry #188
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 | |
| - name: Select Xcode | |
| run: sudo xcode-select -s /Applications/Xcode.app | |
| - name: Resolve Swift packages | |
| run: | | |
| xcodebuild -project Zerm.xcodeproj \ | |
| -scheme Zerm \ | |
| -resolvePackageDependencies \ | |
| -clonedSourcePackagesDirPath .ci-packages \ | |
| 2>&1 | tail -5 | |
| # `make setup` stages whisper and sherpa. There is no target for llama, which is | |
| # built out of band, so the app cannot currently be compiled on a clean machine — | |
| # see the tracking issue. Until that is fixed this job cannot honestly claim to | |
| # build the app, so it asserts the frameworks are present and fails loudly rather | |
| # than reporting success on a build that never happened. | |
| - name: Check native dependencies are available | |
| id: deps | |
| 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 | |
| # set -o pipefail is load-bearing: without it the pipeline takes xcpretty's exit | |
| # code and a failed build reports success. | |
| - 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="" \ | |
| build 2>&1 | xcpretty | |
| # 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 2>&1 | xcpretty | |
| # ── 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" |