Skip to content

Competitor keyword gap matrix (#30–#34) (#52) #70

Competitor keyword gap matrix (#30–#34) (#52)

Competitor keyword gap matrix (#30–#34) (#52) #70

Workflow file for this run

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