Competitor keyword gap matrix (#30–#34) (#52) #70
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 | |
| on: | |
| push: | |
| branches: [main] | |
| # Skip the whole pipeline when a commit only touches non-functional | |
| # files. GitHub's rule is "skip iff every changed file matches one of | |
| # these patterns" — touching any source file still triggers a full run. | |
| # Add to this list as new doc-shaped files appear (CHANGELOG, etc.). | |
| paths-ignore: | |
| - '**/*.md' | |
| - '.gitignore' | |
| - 'LICENSE' | |
| pull_request: | |
| branches: [main] | |
| paths-ignore: | |
| - '**/*.md' | |
| - '.gitignore' | |
| - 'LICENSE' | |
| # Lets us trigger CI manually from the Actions tab — handy when poking | |
| # at a workflow change without pushing extra commits. | |
| workflow_dispatch: | |
| # Cancel in-progress runs on the same ref when a new push arrives. Keeps the | |
| # Actions tab tidy and saves a few CI-minutes for a project that ships small | |
| # personal-tool patches. | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| env-manifest: | |
| name: Env-var contract guard | |
| # Pure bash + grep — Ubuntu is faster and cheaper than macOS for this. | |
| # Runs in parallel with the heavier build jobs so it fans out signal | |
| # early: a PR that drifts the env-var contract sees the failure within | |
| # 30 seconds, not after waiting for the full Swift build. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Run env-manifest guard | |
| run: ./scripts/check-env-manifest.sh | |
| server: | |
| name: Vapor server (swift build + test) | |
| # macos-15 ships Xcode 16+ (with Swift Testing bundled and Swift 6 | |
| # concurrency rules that match the local dev setup). macos-14's default | |
| # Xcode 15.4 doesn't have the `Testing` module and rejects some of our | |
| # weak-self captures. | |
| runs-on: macos-15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # macos-14 ships with Swift 5.10 in the system toolchain — no extra | |
| # setup-action needed. Verify the version so a future runner change | |
| # doesn't silently downgrade us. | |
| - name: Show Swift version | |
| run: swift --version | |
| - name: Cache SwiftPM build artifacts | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| .build | |
| ~/.cache/org.swift.swiftpm | |
| key: ${{ runner.os }}-spm-server-${{ hashFiles('Package.resolved', 'Package.swift') }} | |
| restore-keys: | | |
| ${{ runner.os }}-spm-server- | |
| - name: swift build | |
| run: swift build | |
| - name: swift test | |
| run: swift test | |
| spa: | |
| name: Svelte SPA (type-check + build) | |
| # SPA build is platform-agnostic; Ubuntu is faster and cheaper than macOS | |
| # runners for plain npm work. | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: web | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| # Node 24 — current LTS. Bumped from 20 (M3.23) ahead of | |
| # GitHub Actions' Node 20 deprecation deadline (June 2026) | |
| # and because 20 hit EOL April 2026. Vite 6 + Svelte 5 + | |
| # svelte-check all run clean on 24. | |
| node-version: '24' | |
| # npm ci reads the lockfile — caching the npm store makes the | |
| # 100-ish-package install instant on cache hits. | |
| cache: npm | |
| cache-dependency-path: web/package-lock.json | |
| - name: npm ci | |
| run: npm ci | |
| - name: Type-check (svelte-check) | |
| run: npm run check | |
| - name: Build production bundle | |
| run: npm run build | |
| mac-app: | |
| name: Menubar app (swift build) | |
| # macos-15 ships Xcode 16+ (with Swift Testing bundled and Swift 6 | |
| # concurrency rules that match the local dev setup). macos-14's default | |
| # Xcode 15.4 doesn't have the `Testing` module and rejects some of our | |
| # weak-self captures. | |
| runs-on: macos-15 | |
| defaults: | |
| run: | |
| working-directory: mac | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Show Swift version | |
| run: swift --version | |
| - name: Cache SwiftPM build artifacts | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| mac/.build | |
| ~/.cache/org.swift.swiftpm | |
| key: ${{ runner.os }}-spm-mac-${{ hashFiles('mac/Package.swift') }} | |
| restore-keys: | | |
| ${{ runner.os }}-spm-mac- | |
| - name: swift build | |
| # Builds the SwiftUI MenuBarExtra binary. Bundling into Keywordista.app | |
| # via build-app.sh is intentionally a release-time concern; CI just | |
| # proves the Swift compiles + links. | |
| run: swift build | |
| - name: swift test | |
| # Pure-function regression guards (e.g. ServiceSupervisorEnvTests | |
| # pins the v0.3.5 spawn-env bug shut). No process launches, no | |
| # network, no real backend — those stay in the release pipeline. | |
| run: swift test | |
| image-size: | |
| name: Server image (build + size gate) | |
| # Builds the Dockerfile on every PR — same as release-image.yml does, | |
| # but amd64 only (no QEMU arm64 emulation; release-image.yml owns | |
| # multi-arch) and not pushed anywhere. Catches Dockerfile breakage | |
| # and image-bloat regressions before they hit a real release. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| # GHA layer cache means re-runs on this branch are seconds, not | |
| # minutes. The first run after Dockerfile changes invalidates | |
| # caches for affected layers only. | |
| - name: Build server image (amd64, local load) | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: ./Dockerfile | |
| platforms: linux/amd64 | |
| push: false | |
| load: true | |
| tags: keywordista:ci | |
| build-args: | | |
| KEYWORDISTA_BUILD_VERSION=ci-${{ github.sha }} | |
| KEYWORDISTA_BUILD_COMMIT_SHA=${{ github.sha }} | |
| KEYWORDISTA_BUILD_DATE=${{ github.event.head_commit.timestamp || github.event.pull_request.updated_at }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| # Hard ceiling: 450 MB. The original plan §4.6.1 said <150 MB — | |
| # that turns out to be arm64-only reality (swift:6.1-jammy-slim | |
| # on arm64 is ~120 MB; on amd64 it's ~300+ MB). This CI job | |
| # measures the amd64 build, which is the worst-case platform. | |
| # Current sizes (with dynamic Swift stdlib, swift:6.1-jammy-slim): | |
| # arm64 (local): ~130 MB | |
| # amd64 (CI): ~360 MB (rough; refined as more builds land) | |
| # Real shrink path is M6 polish: distroless/cc + manual Swift | |
| # runtime lib bundling can probably land both arches under | |
| # 100 MB. Not M0 work. | |
| # | |
| # The gate still has value: a ceiling 25% above current amd64 | |
| # blocks "we shipped Foundation twice by accident"-style | |
| # regressions while not blocking on inherent base-image size. | |
| - name: Enforce image-size ceiling (450 MB, amd64 worst-case) | |
| run: | | |
| set -euo pipefail | |
| BYTES=$(docker image inspect keywordista:ci --format '{{.Size}}') | |
| MB=$(( BYTES / 1024 / 1024 )) | |
| CEILING=450 | |
| echo "→ keywordista:ci is ${MB} MB (ceiling: ${CEILING} MB)" | |
| if (( MB > CEILING )); then | |
| echo "✘ image grew past the ${CEILING} MB ceiling." | |
| echo " If the growth is intentional, raise the ceiling in" | |
| echo " .github/workflows/ci.yml and document the reason." | |
| exit 1 | |
| fi | |
| # Surface the size on the GHA run summary so trends are | |
| # visible in the Actions tab over time. | |
| { | |
| echo "## Image size" | |
| echo "" | |
| echo "**${MB} MB** / ${CEILING} MB ceiling (amd64)" | |
| echo "" | |
| echo "_M6 polish opportunity: distroless/cc + manual Swift" | |
| echo "runtime lib bundling can plausibly cut this by 60%+._" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| # End-to-end boot smoke: a sealed image that doesn't actually run | |
| # is useless. Same shape as `make docker-smoke` locally. | |
| - name: Smoke test (boot + /health) | |
| run: | | |
| set -euo pipefail | |
| docker run -d --name kw-smoke \ | |
| -p 8080:8080 \ | |
| -e KEYWORDISTA_ENCRYPTION_KEY=$(openssl rand -hex 32) \ | |
| -e KEYWORDISTA_PUBLIC_BASE_URL=http://localhost:8080 \ | |
| keywordista:ci | |
| # Wait up to 30s for /health to become 200. | |
| for i in $(seq 1 15); do | |
| if curl -fsS http://localhost:8080/health > /dev/null 2>&1; then | |
| echo "✓ /health is green after ${i}×2s" | |
| docker stop kw-smoke > /dev/null | |
| exit 0 | |
| fi | |
| sleep 2 | |
| done | |
| echo "✘ /health didn't respond within 30s" | |
| docker logs kw-smoke | |
| docker stop kw-smoke > /dev/null | |
| exit 1 |