Engagement Phase 1 (docs/website/ENGAGEMENT.md §1.2a, §3.1, §3.2). A subtraction and a replacement in one commit, because leaving the OAuth2 flow half-wired across a release is worse than either end state. Deleted, per the §1.2a inventory: GET /admin/email/connect/start and /connect/callback, the connectStart/connectCallback controllers with the email_oauth_tx signed cookie, the PKCE verifier and CSRF nonce plumbing, the https://mail.google.com/ scope, the borrowed `google` auth-providers client, the OAuth2 nodemailer transport with its smtp.gmail.com:465 literals, the refresh-token decrypt in the model, and the client's Connect Gmail button, redirect banner and six Gmail error strings. `provider` and `refresh_token_enc` stay as columns under the additive-only discipline, unread. Added: a mail transport registry (server/src/engagement/transports) with `smtp` as the sole registration. `credentialFields` is the single declaration the admin form renders, the sanitizer filters against, and the "is it secret" answer comes from, so adding a transport is a registration rather than four edits. email_config gains transport / credential_enc (one encrypted JSON blob, since the field list is the transport's to declare) / reply_to. All six call sites keep their exact failure contracts: the contact form's mailto fallback, the invite's copyable link, the reset's generic 200, and sendTeamNotification's never-throws. One deliberate behaviour change: `enabled` now gates every sender rather than only isConfigured() — the connect flow used to set it as a side effect, and with a credential form the toggle has to mean what it says. Send-test becomes the real verification. Under OAuth2 the sender came back from Google and was guaranteed to belong to the credential; operator-typed, it can be refused, so failures name the sender and the SPF/DMARC reason (§1.2a consequence 2). G22, the silent degradation: an upgraded deployment backfills to smtp with no credentials and every sink politely does nothing. The admin dashboard now warns when the deprecated Gmail token is present and no replacement credential is, so the one deployment this happens to is told. A fresh install has never had mail and is not nagged. Guardrails: new `npm run check:hosts` (§3.2 rule 4) with its own self-test, wired into pr-checks before the install; routes.manifest and routes.guards regenerated (-2 routes). Co-Authored-By: Claude <noreply@anthropic.com>
116 lines
5.1 KiB
YAML
116 lines
5.1 KiB
YAML
# Gate every pull request into `main` or `edge` on a fast, DB-free check suite so
|
|
# a broken build or failing test can't reach either integration branch. Complements
|
|
# build-images.yml, which runs only AFTER merge (on push to main) to publish
|
|
# images — this one runs BEFORE merge.
|
|
#
|
|
# `edge` is listed as well as `main` because long workstreams land phase by phase
|
|
# on `edge` and reach `main` as a single cutover (the module system, protocol v3).
|
|
# With `branches: [main]` alone, every one of those phase PRs merges with NO checks
|
|
# at all and the entire workstream runs blind until the cutover — which is exactly
|
|
# what happened to the nine Android M12 phase PRs in that repo. A branch that
|
|
# accumulates work for weeks needs the gate more than `main` does, not less.
|
|
#
|
|
# Enforcement (one-time, in the Gitea UI):
|
|
# Repository Settings → Branches → Branch Protection (rule for `main`)
|
|
# • Enable Status Check
|
|
# • Status check patterns: PR Checks / *
|
|
# Note: Gitea only lists a context in its dropdown after it has reported once,
|
|
# so let this workflow run on one PR first. The `PR Checks / *` glob matches
|
|
# without needing the dropdown.
|
|
# The workflow now RUNS on PRs into `edge` too, but running is not enforcing:
|
|
# blocking a red phase PR needs its own protection rule for `edge`, with the
|
|
# same `PR Checks / *` pattern. Without one the checks report and merging stays
|
|
# possible anyway.
|
|
#
|
|
# Runner: reuses the existing self-hosted `ubuntu-latest` runner. These jobs need
|
|
# only Node (no Docker socket), and the server tests stub their models + point the
|
|
# DB pool at a dead port, so no MariaDB service is required.
|
|
|
|
name: PR Checks
|
|
|
|
on:
|
|
pull_request:
|
|
branches: [main, edge]
|
|
|
|
# A newer push to the same PR cancels the in-flight run.
|
|
concurrency:
|
|
group: pr-checks-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
server-tests:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
cache: npm
|
|
cache-dependency-path: server/package-lock.json
|
|
- name: Check core names no module identifier
|
|
# Phase 3's acceptance criterion 1 (MODULE_API.md §5.2): core must not
|
|
# name a module's files, import them, route to them, or declare its
|
|
# symbols. Before `npm ci`, deliberately — it is plain Node over
|
|
# server/ and client/ source with no dependency of its own, so putting it
|
|
# first makes a boundary break the first thing a reviewer sees instead of
|
|
# something found under a pile of unrelated failures, and it costs
|
|
# nothing when it passes.
|
|
run: npm run check:modules
|
|
- name: Check the engagement subsystem names no external host
|
|
# ENGAGEMENT.md §3.2 rule 4 — no transport may ship a default host,
|
|
# endpoint or sender. Dependency-free and runs before the install for the
|
|
# same reason as the check above: a phone-home is a design break, not a
|
|
# test failure, and it should be the first thing a reviewer sees.
|
|
run: npm run check:hosts
|
|
- name: Install server deps
|
|
run: npm ci --prefix server
|
|
- name: Run server tests
|
|
run: npm test --prefix server
|
|
|
|
- name: Check the route manifest is current
|
|
# The URL surface is frozen while the routers are carved up by capability
|
|
# (docs/website/API_V2_PLAN.md § Phase 2). Regenerating from the live Express
|
|
# stack and diffing proves a "mechanical" refactor moved no URL. A PR that
|
|
# really does change one has to commit the new manifest, putting it in front
|
|
# of a reviewer instead of letting it pass silently.
|
|
run: npm run routes:manifest --prefix server -- --check
|
|
|
|
client-build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
cache: npm
|
|
cache-dependency-path: client/package-lock.json
|
|
- name: Install client deps
|
|
run: npm ci --prefix client
|
|
- name: Run client tests
|
|
# Pure-logic unit tests on Node's built-in runner (no browser/DOM).
|
|
run: npm test --prefix client
|
|
- name: Build client
|
|
run: npm run build --prefix client
|
|
|
|
bot-tests:
|
|
# The install still runs first and still catches a broken or out-of-sync
|
|
# lockfile before it ships in the bot image — that was this job's whole
|
|
# purpose until phase 7 (TEAMS.md §7.1) put real logic in the bot: it now
|
|
# pulls slash-command definitions from the app, merges them into the
|
|
# whole-set PUT, and runs the defer→dispatch→edit path. None of that is
|
|
# reachable from the server suite, and phases 8 and 9 add more of it.
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
cache: npm
|
|
cache-dependency-path: bot/package-lock.json
|
|
- name: Install bot deps
|
|
run: npm ci --prefix bot
|
|
- name: Run bot tests
|
|
# Node's built-in runner, no browser and no Discord connection — the
|
|
# interaction is a fake that records what was called on it.
|
|
run: npm test --prefix bot
|