Files
website/.gitea/workflows/pr-checks.yml
Claude cecd72915f feat(teams): the slash-command seam, and the first command through it
Phase 7 of TEAMS.md. `api.registerSlashCommands` stops throwing: a module
registers a command's DEFINITION and its HANDLER together, the bot pulls the
definitions over the internal listener and runs none of our code, and the
handler executes here — forced by the bot container having no `modules` volume,
and the right boundary anyway.

Registration validates what Discord would reject as a batch (names, description
lengths, the four option types, required-before-optional), because the bot
registers the whole set in one PUT and a single bad entry costs every command
including the bot's own. Commands are not namespaced under their owner — there
is no dot in Discord's name grammar — so collisions are first-come with the
holder named.

The dispatcher is the access boundary: `linked` has no Discord equivalent, so
the platform-side permission default can only ever be advertising. It resolves
the actor by `auth_providers.kind` rather than the id slug, treats a banned
account as unlinked, bounds a handler under the bot's own timeout, and keeps
`ok` outside the envelope so a handler cannot forge it.

Liveness is asked at both the pull and the dispatch. The registries have no
removal path, so a module an operator disables at runtime would otherwise keep
a live handler behind a command Discord still advertises.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 18:53:34 -05:00

110 lines
4.7 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: 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