Files
warmbly/.github/workflows/ci.yml
T
Matthew Meszaros e143cb0628 feat: give warmblyctl an API-key half so agents and scripts can operate any Warmbly instance, ship in-repo agent skills that teach it, and cut the README quick start and self-hosting sections down to commands plus docs links (#135)
* feat: cut the README quick start and self-hosting sections down to the install command, one paragraph of what it does, and links out to the local development, first-run, deployment and warmblyctl docs pages, dropping the recovery if/then table, the MAIL_TRANSPORT invitation note and the dependency matrix that all duplicate those pages

* feat: give warmblyctl an API-key half so agents and scripts can operate any Warmbly instance including the hosted one, adding a raw passthrough (warmblyctl api get/post/patch/put/delete <path> with --data taking a literal, - or @file, and --idempotency-key) plus eleven typed families (me, campaign, contact, mailbox, inbox, analytics, settings, webhook, apikey, template, crm) driven by one spec table that generates dispatch, flags, per-command help and the request, covering list/get/create/update/delete, sequence steps, sender pools, preflight/start/stop/test-email, contact notes/timeline/import/export, mailbox behavior/verify/send and the six warmup controls, unibox threads/reply/compose/seen/agent-drafts/scheduled sends, outreach settings, webhook secrets and deliveries, and API key self-service, authenticated with Bearer wmbly_ keys from WARMBLY_API_KEY against WARMBLY_API_URL falling back to API_PUBLIC_URL then the hosted service, printing the API's JSON untouched and surfacing the error envelope's code and request_id with Retry-After on 429, while the DB-direct operator commands and their trust model stay exactly as they were

* feat: ship two in-repo agent skills so AI assistants working against Warmbly discover the right warmblyctl half on their own, .claude/skills/warmbly-api teaching product operation over the API commands (key and URL setup including the seeded local dev key, the eleven command families, pagination and error-code and Idempotency-Key conventions, and a sending-safety section that names the six commands that put real mail on the wire and holds agents to preflight before start and the 50/day default cap) and .claude/skills/warmbly-ops teaching instance administration over the DB-direct commands (status --json as the contract to parse, the recovery command table, TTY versus -T piping, Redis-down behaviour, and org export/import handling including --dry-run first and the sensitivity of credential archives), each pointing at the other for what it does not cover, narrowing the .gitignore .claude/ rule to .claude/* with !.claude/skills/ so personal agent state stays local while the skills ship

* feat: document warmblyctl's new API half on the warmblyctl reference page, reframing the intro around the two halves and their two trust models and replacing the 'no HTTP surface and never will' line with the accurate claim that the CLI never serves HTTP while the API commands are a client of the already-gated public API, adding WARMBLY_API_KEY and WARMBLY_API_URL to the environment table with the API_PUBLIC_URL-then-hosted fallback, renaming The commands to The operator commands, and adding an API commands section covering key setup, the eleven command families, the raw /v1 passthrough with curl-style --data forms, the pagination, idempotency-key and Retry-After conventions, a warning callout naming the six commands that put real mail on the wire with preflight-before-start guidance, and a pointer to the shipped .claude/skills agent skills, plus API authentication and permissions links in See also

* feat: move the shipped agent skills from .claude/skills/ to a top-level skills/ directory so they follow the convention other repos use for distributable agent skills rather than living inside Claude Code's personal state directory, restoring the .gitignore .claude/ rule to its original form since nothing tracked lives under it anymore, and updating the warmblyctl reference's For AI agents section to name skills/ and show installing a skill by copying it into the agent's own skills directory or pointing the agent at SKILL.md directly

* feat: clear the Security Scan failure by lifting the two flagged indirect Go modules past their fixed versions, github.com/moby/go-archive from v0.2.0 to v0.3.0 for the CVE-2026-17106 tar path traversal and golang.org/x/mod from v0.37.0 to v0.40.0 for the CVE-2026-56864 and CVE-2026-56865 GOSUMDB and GOPROXY forgery pair, with the x/sys, x/text and x/tools bumps go mod tidy pulls along

* feat: take the Trivy dependency scan off the PR gate and restructure CI the way larger projects do, because a full-repo CVE scan on every pull request goes red the morning any dependency gets a new advisory regardless of what the PR touches, which is exactly how this branch failed on two indirect Go modules it never went near, moving the scan to its own security.yml running weekly, on demand, and on main pushes that change a dependency manifest, pinned to trivy-action 0.36.0 instead of @master, extracting the pnpm+Node+frozen-install boilerplate repeated across the web, admin and site jobs into a .github/actions/setup-pnpm composite action with the store cached per lockfile, and collapsing the CI Status rollup's ten hand-enumerated result checks that had to be edited in two places per new job into a single contains(needs.*.result, ...) expression over failure and cancelled, all validated with actionlint
2026-08-19 20:59:40 -07:00

350 lines
9.5 KiB
YAML

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
# Default token only gets `contents: read` on PRs from forks; the
# dorny/paths-filter action needs to list PR files via the GitHub
# API, which requires `pull-requests: read`. Without this the
# "Detect Changes" job dies with "Bad credentials" and every
# downstream language CI is skipped.
permissions:
contents: read
pull-requests: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
changes:
name: Detect Changes
runs-on: ubuntu-latest
outputs:
go-services: ${{ steps.filter.outputs.go-services }}
tracking: ${{ steps.filter.outputs.tracking }}
realtime: ${{ steps.filter.outputs.realtime }}
web: ${{ steps.filter.outputs.web }}
admin: ${{ steps.filter.outputs.admin }}
site: ${{ steps.filter.outputs.site }}
make: ${{ steps.filter.outputs.make }}
ios: ${{ steps.filter.outputs.ios }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
go-services:
- 'go.mod'
- 'go.sum'
- 'internal/**'
- 'cmd/**'
tracking:
- 'tracking/**'
realtime:
- 'realtime/**'
- 'deploy/docker/realtime.Dockerfile'
web:
- 'web/**'
admin:
- 'admin/**'
site:
- 'site/**'
make:
- 'integrations/make/**'
ios:
- 'ios/**'
go-ci:
name: Go CI
needs: changes
if: needs.changes.outputs.go-services == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Set up protoc
uses: arduino/setup-protoc@v3
with:
version: "33.x"
repo-token: ${{ github.token }}
- name: Install required Go tools
run: make setup-tools
- name: Run golangci-lint
run: make lint
- name: Verify protobuf files are up to date
run: make check-proto
- name: Run tests with coverage
run: |
go test -race -coverprofile=coverage.out -covermode=atomic ./...
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: coverage.out
flags: go
fail_ci_if_error: false
web-ci:
name: Web CI
needs: changes
if: needs.changes.outputs.web == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: web
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-pnpm
with:
working-directory: web
- name: Lint
run: pnpm lint
- name: Typecheck
run: pnpm typecheck
- name: Run tests
run: pnpm test:run
- name: Build
run: pnpm build
admin-ci:
name: Admin CI
needs: changes
if: needs.changes.outputs.admin == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: admin
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-pnpm
with:
working-directory: admin
- name: Lint
run: pnpm lint
- name: Typecheck
run: pnpm typecheck
- name: Build
run: pnpm build
site-ci:
name: Site CI
needs: changes
if: needs.changes.outputs.site == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: site
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-pnpm
with:
working-directory: site
node-version: "22"
- name: Build
# Astro project; build catches type errors, broken imports,
# missing assets. No separate lint/typecheck script is wired
# in package.json today.
run: pnpm build
make-ci:
name: Make App CI
needs: changes
if: needs.changes.outputs.make == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: integrations/make
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Validate Make app
# IML is interpreted by Make (no compile step); validate.mjs checks the
# manifest, JSON validity, code-file references, and component enums.
run: node scripts/validate.mjs
rust-ci:
name: Rust CI
needs: changes
if: needs.changes.outputs.tracking == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: tracking
steps:
- uses: actions/checkout@v4
# The default tracking build is NATS-only (pure Rust, rustls TLS), so it
# needs no librdkafka / libcurl / libsasl system headers. Those are only
# required for the optional `--features kafka` build.
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Cache Cargo
uses: Swatinem/rust-cache@v2
with:
workspaces: tracking
- name: Check formatting
run: cargo fmt --check
- name: Run Clippy
run: cargo clippy -- -D warnings
- name: Run tests
run: cargo test
elixir-ci:
name: Elixir CI
needs: changes
if: needs.changes.outputs.realtime == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: realtime
env:
MIX_ENV: test
steps:
- uses: actions/checkout@v4
- name: Set up Elixir
# mix.exs requires `elixir: "~> 1.18"`. Bumped here to match.
# Phoenix 1.8 + plug 1.19 also expect a recent OTP.
uses: erlef/setup-beam@v1
with:
elixir-version: "1.18"
otp-version: "27"
- name: Cache deps
uses: actions/cache@v4
with:
path: |
realtime/deps
realtime/_build
key: ${{ runner.os }}-mix-${{ hashFiles('realtime/mix.lock') }}
restore-keys: ${{ runner.os }}-mix-
- name: Install dependencies
run: mix deps.get
- name: Check formatting
run: mix format --check-formatted
- name: Run Credo
# credo isn't in mix.exs yet; skip when the binary isn't
# available so CI doesn't false-fail. Re-enable once it's
# added as a dev dep.
run: mix help credo > /dev/null 2>&1 && mix credo --strict || echo "credo not installed; skipping"
- name: Compile
# Don't fail the build on transitive warnings — jose/CAStore
# and a couple of our own files emit deprecation warnings on
# Elixir 1.18 that aren't fixable without forking deps.
# Real compile errors still fail the step.
run: mix compile
- name: Run tests
run: mix test
ios-ci:
name: iOS CI
needs: changes
if: needs.changes.outputs.ios == 'true'
# macOS minutes are 10x Linux; the paths filter keeps this off
# non-iOS PRs. The app needs the iOS 26 SDK and the Icon Composer
# .icon app icon, so it must be a macos-26 image.
runs-on: macos-26
steps:
- uses: actions/checkout@v4
- name: Select newest Xcode
run: |
sudo xcode-select -s "$(ls -d /Applications/Xcode*.app | sort -V | tail -1)"
xcodebuild -version
- name: Build (iOS Simulator, no signing)
# arm64 only: the generic destination otherwise also builds the
# x86_64 slice, doubling build time for a compile check.
run: |
set -o pipefail
xcodebuild build \
-project ios/Warmbly.xcodeproj \
-scheme Warmbly \
-destination 'generic/platform=iOS Simulator' \
ARCHS=arm64 ONLY_ACTIVE_ARCH=YES \
CODE_SIGNING_ALLOWED=NO
# Validate the web/admin production Dockerfiles (build + nginx config check)
# without pushing, so a broken image can't reach a release tag unnoticed. The
# release workflow is the only other place these build, and only on tags.
frontend-images:
name: Frontend Image (${{ matrix.service }})
needs: changes
if: needs.changes.outputs.web == 'true' || needs.changes.outputs.admin == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
service: [web, admin]
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build image (no push)
uses: docker/build-push-action@v6
with:
context: ./${{ matrix.service }}
file: ./${{ matrix.service }}/Dockerfile
push: false
platforms: linux/amd64
cache-from: type=gha,scope=ci-${{ matrix.service }}
cache-to: type=gha,mode=max,scope=ci-${{ matrix.service }}
# The one job to mark required in branch protection. Skipped jobs pass
# (their tree did not change); anything failed or cancelled fails it.
# Dependency scanning lives in security.yml, off the PR path on purpose.
ci-status:
name: CI Status
runs-on: ubuntu-latest
needs: [changes, go-ci, web-ci, admin-ci, site-ci, make-ci, rust-ci, elixir-ci, ios-ci, frontend-images]
if: always()
steps:
- name: Check CI status
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1