Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
cbc161d
feat(torznab): the seeding rules a client was never told, and rates t…
Dim145 Aug 31, 2026
efe9845
feat(tracker): the second infohash a hybrid torrent announces under
Dim145 Aug 31, 2026
44b5f41
feat(web): installable, with an icon size nobody made up
Dim145 Aug 31, 2026
083655d
feat(me): a copy of your own record, and what it deliberately leaves out
Dim145 Aug 31, 2026
5e80e32
feat(admin): who did what, across the whole console
Dim145 Aug 31, 2026
8cfd071
docs(roadmap): the five that shipped, and two that shipped a while ago
Dim145 Aug 31, 2026
067f2be
feat(bonus): a freeleech you can put on one torrent
Dim145 Sep 1, 2026
0df67d7
feat(tracker): a partial seed is not a seed
Dim145 Sep 1, 2026
939111d
feat(torrents): three things a catalogue needs when it gets old
Dim145 Sep 1, 2026
3bcf60c
feat(me): three keys, because one was doing three jobs
Dim145 Sep 1, 2026
d13d5a8
feat(me): tell me when something like this turns up
Dim145 Sep 1, 2026
36c4bbc
feat(auth): where an account has been signed in from
Dim145 Sep 1, 2026
1965a23
feat(admin): who vouched for them, and who they let in
Dim145 Sep 1, 2026
b1dbae9
feat(torznab): a Prowlarr definition, generated per instance
Dim145 Sep 1, 2026
19a664d
fix(api): a bad filter on the audit log was a 500
Dim145 Sep 1, 2026
98e3be2
docs(roadmap): eight lines that are no longer wishes
Dim145 Sep 1, 2026
0c47ec6
fix(torznab): a block a member could lift by rotating their own key
Dim145 Sep 1, 2026
6915bdb
fix(themes): the contrast gate was not looking at the field it types …
Dim145 Sep 1, 2026
c2ad7e1
feat(stats): the site, looking at itself
Dim145 Sep 1, 2026
5d4ac27
feat(irc): one line in a channel, and the parser for it in the same b…
Dim145 Sep 1, 2026
e0e1b45
fix(torznab): the feed published the announce passkey to read-key hol…
Dim145 Sep 1, 2026
b2c6648
fix(irc): the queue paced nothing, and two thirds of announces never …
Dim145 Sep 1, 2026
ededfe1
fix(stats): the adult opt-in never worked, and four figures were wrong
Dim145 Sep 1, 2026
57755cb
fix(privacy): the register, the history, the export and the erasure
Dim145 Sep 1, 2026
5749e8c
fix(tracker): an unauthenticated scrape could reach Postgres, and two…
Dim145 Sep 1, 2026
481dd4e
fix(ui): three pages rendered bare native buttons, and the toast said…
Dim145 Sep 1, 2026
6c5d8d6
fix(web): a date in the future is not an age, and a bar must fit its …
Dim145 Sep 1, 2026
0f570c1
fix(stats): the page's title outweighed its numbers, and the charts o…
Dim145 Sep 1, 2026
c6637d2
fix(alerts): an armed alert looked exactly like a silent one, and the…
Dim145 Sep 1, 2026
3b25bd5
fix(me): rotating a key you had not revealed threw the new one away
Dim145 Sep 1, 2026
30138a2
fix(security): one address on five days read as five different places
Dim145 Sep 1, 2026
72222f8
fix(irc): the test button destroyed unsaved edits, and the state bloc…
Dim145 Sep 1, 2026
6044e5e
fix(audit): a fresh install read as a broken page, and the register w…
Dim145 Sep 1, 2026
19667b0
fix(invites): the chain was flat, the truncation notice was under the…
Dim145 Sep 1, 2026
172de5a
fix(themes): the contrast warning printed English inside a French sen…
Dim145 Sep 1, 2026
6058b1c
fix(torrents): a live freeleech advertised itself as already over
Dim145 Sep 1, 2026
b7f9a71
docs: what the interface pass changed, and what the contrast gate can…
Dim145 Sep 1, 2026
0bc7bce
fix(api): a malformed query parameter answered 500, not 400
Dim145 Sep 2, 2026
3a62385
fix(admin): the dashboard's tracker light was a constant, not a status
Dim145 Sep 2, 2026
609c6b4
fix(web): two pages shipped the server's clock and corrected it on hy…
Dim145 Sep 2, 2026
bd16c2a
fix(alerts): a saved search rebuilt its link with the API's parameter…
Dim145 Sep 2, 2026
1a8b36b
fix(api): a saved search carrying a tag could not be created at all
Dim145 Sep 2, 2026
fb17a69
fix(forum): a post from twelve minutes ago read as two hours old
Dim145 Sep 2, 2026
7bc83b4
fix(messaging): stop() then start() left an orphan stream delivering …
Dim145 Sep 2, 2026
f135299
fix(caddy): the credential scrub in the access log had never matched …
Dim145 Sep 2, 2026
d97bfd1
refactor(api): the role cache moved out of adminAuth, and re-exportin…
Dim145 Sep 2, 2026
93a8fa8
fix(security): the API served a CSP written for the web application
Dim145 Sep 2, 2026
cfbf60e
fix(security): an IP ban locked staff out of the route that lifts it
Dim145 Sep 2, 2026
3e6d848
fix(tracker): a hundred concurrent peer ids reached a hundred GiB/s o…
Dim145 Sep 2, 2026
14df017
fix(db): a case-different username was a second account, and the FKs …
Dim145 Sep 2, 2026
dca011f
fix(ci): the documentation was built and deployed on a Node that is e…
Dim145 Sep 2, 2026
60cb560
fix(docker): Postgres trusted the network, and Caddy had no body limit
Dim145 Sep 2, 2026
222b46c
chore(deps): sixteen updates, and the two version decisions worth wri…
Dim145 Sep 2, 2026
947aa84
test(e2e): the forum and the ticket queue were empty on a kept stack
Dim145 Sep 2, 2026
9788a17
test(api): the panic suite timed out on its own key-derivation cost
Dim145 Sep 2, 2026
387df95
fix(panic): a twelve-character password was derived at Node's defaults
Dim145 Sep 2, 2026
0ba6daa
fix(auth): one proof of work could be spent twice, and a name differe…
Dim145 Sep 2, 2026
02c211c
fix(channels): a webhook reflected its target's response back to the …
Dim145 Sep 2, 2026
347a9bb
fix(api): an announce could claim more bytes than a number can hold
Dim145 Sep 2, 2026
d5d50b7
fix(torrents): a wrong type in an edit reached Postgres as a 500
Dim145 Sep 2, 2026
deb0918
feat(web): keep what is being written until it is sent
Dim145 Sep 2, 2026
96f9c28
fix(web): an error page in English, and numbers formatted in the wron…
Dim145 Sep 2, 2026
4d9b63f
fix(web): four systemic faults behind five hundred interface findings
Dim145 Sep 2, 2026
fe8db1f
fix(shared): tighten the DID proof and the theme token helpers
Dim145 Sep 2, 2026
61eae16
docs: three pages promised protection the code does not provide
Dim145 Sep 2, 2026
87f3a69
fix(api): the freeleech pool named its top members to anonymous callers
Dim145 Sep 2, 2026
6671993
fix(panic): encrypt was an unbounded oracle against the panic password
Dim145 Sep 2, 2026
776d746
fix(forum): a topic author could delete everyone else's replies
Dim145 Sep 2, 2026
ea8ce0f
fix(api): the upload count contradicted the anonymous-uploads setting
Dim145 Sep 2, 2026
bac5d58
fix(forum): the last two mutating routes without a schema
Dim145 Sep 2, 2026
3d49573
fix(me): the one verifyTotp call without its replay guard, and a stam…
Dim145 Sep 2, 2026
b1d588a
fix(security): a banned member kept the account routes under /api/auth
Dim145 Sep 2, 2026
4113ba0
fix(torrents): a takedown left the catalogue, the page and the file u…
Dim145 Sep 2, 2026
d0f894e
feat(auth): sign out everywhere
Dim145 Sep 2, 2026
00a3482
fix(relay): a race unsubscribed a channel for good, and the drain sig…
Dim145 Sep 2, 2026
e5168ed
ci(relay): the relay had no workflow at all
Dim145 Sep 2, 2026
66d2b87
fix(tracker): the swarm-cache ceiling counted traffic, not memory
Dim145 Sep 2, 2026
a4bf1fe
fix(tracker): budget spent before the credit was decided, and dead co…
Dim145 Sep 2, 2026
b244b8c
feat(tracker): let an operator close UDP scrape, and stop flooding th…
Dim145 Sep 2, 2026
52c26dd
docs: /api/health publishes more than the reference admitted
Dim145 Sep 2, 2026
f624d47
fix(web): the sanitiser passed a phishing link and ate legitimate att…
Dim145 Sep 2, 2026
dd597d4
fix(web): the path guard did not see %2e%2e
Dim145 Sep 2, 2026
8ee6dd4
fix(web): the bell died on reconnect, and three more values differed …
Dim145 Sep 2, 2026
1c8515e
fix(web): a modal with no focus trap, and a group row only a mouse co…
Dim145 Sep 2, 2026
49eff95
fix(web): 49 form controls had no accessible name
Dim145 Sep 2, 2026
40199bd
fix(web): document.write on the recovery-codes screen
Dim145 Sep 2, 2026
c076dde
fix(web): two locales hardcoded in a bilingual site
Dim145 Sep 2, 2026
c875ee7
feat(web): the "sign out everywhere" control
Dim145 Sep 2, 2026
0641035
fix(tracker): a failed seed-time write blocked its own retry for 900 …
Dim145 Sep 2, 2026
ebf9ade
perf(db): hnr_tracking updates could not be HOT, so each one wrote se…
Dim145 Sep 2, 2026
1053ab3
fix(db): 0071 measured a benchmark artefact, not hnr_tracking
Dim145 Sep 3, 2026
d3322be
perf(tracker): a member seeding 70 torrents wrote their own row 70 ti…
Dim145 Sep 3, 2026
2eda5b3
fix(api): safeFetch handed the caller's credentials to whoever asked …
Dim145 Sep 3, 2026
047fba5
fix(tracker): closing the UDP socket stopped the loop, not the work i…
Dim145 Sep 3, 2026
d7d4f08
fix(web): a session revoked from another device showed a raw failure
Dim145 Sep 3, 2026
718584b
fix(web): 24 form controls announced themselves as "text field" and n…
Dim145 Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
50 changes: 42 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,22 @@ IMAGE_TAG=latest
# Development (SSL disabled)
DATABASE_URL=postgres://tracker:tracker@localhost:5432/trackarr

# Production (SSL enforced - sslmode=require automatically added in nuxt.config.ts)
# DATABASE_URL=postgres://tracker:STRONG_PASSWORD@pgbouncer:6432/trackarr
# En production, `docker-compose.prod.yml` construit `DATABASE_URL` lui-même à
# partir de DB_USER / DB_PASSWORD / DB_NAME : ne pas la renseigner ici.
#
# Ce bloc annonçait « sslmode=require automatically added in nuxt.config.ts ».
# Deux erreurs : le calcul est dans `apps/api/nitro.config.ts`, et la valeur
# qu'il produit (`runtimeConfig.databaseUrl`) N'EST LUE NULLE PART —
# `packages/db/src/index.ts` lit `process.env.DATABASE_URL` directement. Rien
# n'ajoute donc `sslmode=require` automatiquement. Sur le réseau interne de
# Docker, TLS est délibérément coupé (`-c ssl=off` côté Postgres,
# `sslmode=disable` dans la DSN) ; pour une base externe, mettre `DB_SSL=true`
# et l'écrire dans la DSN soi-même.

# Production database settings
DB_USER=tracker
# ⚠️ PRODUCTION: Generate with: openssl rand -base64 32
# ⚠️ PRODUCTION: Generate with: openssl rand -hex 32
# (hex, pas base64 : un « / » dans le mot de passe casse la DSN)
# Leave empty and let the stack refuse to start rather than ship a guessable
# default. Generate one with the command block at the bottom of this file.
DB_PASSWORD=
Expand All @@ -49,9 +59,15 @@ DB_PORT=5432
DB_POOL_MAX=20

# SSL/TLS for database connections
# Development: SSL disabled for local testing
# Production: SSL automatically enforced (see documentation/ssl-setup.md)
# Generate certificates with: ./scripts/generate-ssl-certs.sh
#
# Coupé par défaut, y compris en production : la base n'est joignable que depuis
# le réseau interne de Docker et n'est pas publiée. Pour une base EXTERNE ou
# gérée, mettre `DB_SSL=true` et ajouter `?sslmode=require` à la DSN.
#
# (Les deux renvois qui figuraient ici — `documentation/ssl-setup.md` et
# `./scripts/generate-ssl-certs.sh` — ne correspondent à aucun fichier du
# dépôt.)
DB_SSL=false

# Debug mode (never enable in production)
DB_DEBUG=false
Expand Down Expand Up @@ -214,10 +230,28 @@ MESSAGING_SERVICE_URL=/messaging
# echo "NUXT_SESSION_SECRET=$(openssl rand -hex 32)" >> .env
# echo "ADMIN_API_KEY=$(openssl rand -hex 32)" >> .env
# echo "IP_HASH_SECRET=$(openssl rand -hex 32)" >> .env
# echo "DB_PASSWORD=$(openssl rand -base64 24)" >> .env
# echo "REDIS_PASSWORD=$(openssl rand -base64 24)" >> .env
# `-hex`, pas `-base64` : l'alphabet base64 contient « / », qui termine
# l'autorité d'une URL. Le mot de passe est injecté dans
# `postgres://tracker:<pass>@pgbouncer:6432/…`, donc deux mots de passe sur
# cinq (mesuré : 0,395) produisaient une DSN illisible et une pile qui ne
# joignait pas sa base, avec une erreur « Invalid URL » qu'aucune page de doc
# ne relie au générateur. `doc/guide/local-production.md` employait déjà la
# bonne forme.
# echo "DB_PASSWORD=$(openssl rand -hex 32)" >> .env
# echo "REDIS_PASSWORD=$(openssl rand -hex 32)" >> .env
# # Configure domains
# echo "DOMAIN=your-tracker.com" >> .env
# # Start
# docker compose -f docker-compose.prod.yml up -d

# Hôtes autorisés pour le canal webhook, séparés par des virgules.
#
# L'URL d'un webhook est un champ de MEMBRE : chacun renseigne la sienne, sans
# revue d'administrateur. `safeFetch` écarte déjà les plages privées, la boucle
# locale et le lien-local, et re-valide chaque redirection — il reste une course
# de réattachement DNS sous-milliseconde, qui demande un serveur DNS contrôlé.
#
# Vide (le défaut) = aucune restriction, comportement inchangé. Renseigné, seuls
# ces hôtes et leurs sous-domaines sont joignables.
# WEBHOOK_ALLOW_HOSTS=discord.com,hooks.slack.com
WEBHOOK_ALLOW_HOSTS=
8 changes: 7 additions & 1 deletion .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,13 @@ jobs:
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20
# Node 24, comme `doc/Dockerfile` et comme les autres workflows.
# Ce travail était resté sur 20, en fin de vie depuis avril 2026 :
# la même documentation était donc construite sur deux runtimes,
# et le déploiement — qui a `pages: write` — tournait sur celui
# qui ne reçoit plus de correctif. Vérifié : `npm ci` puis
# `npm run build` passent en 24 (vitepress 1.6.4 demande >= 20).
node-version: 24
cache: 'npm'
cache-dependency-path: './doc/package-lock.json'

Expand Down
66 changes: 66 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64
# Un SBOM et une attestation de provenance, attachés à l'image.
#
# Les images sont déjà signées par cosign en mode sans clé juste en
# dessous : la signature dit QUI a poussé, elle ne dit pas CE QUE
# l'image contient. Sur un projet que des opérateurs tiers installent,
# c'est la moitié qui manquait — un `syft`/`grype` sur l'image publiée
# n'avait rien à lire, et un avis de sécurité sur une dépendance
# transitive ne se recoupait pas avec les versions réellement
# embarquées.
sbom: true
provenance: mode=max

- name: Sign the api image
env:
Expand Down Expand Up @@ -165,6 +176,17 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64
# Un SBOM et une attestation de provenance, attachés à l'image.
#
# Les images sont déjà signées par cosign en mode sans clé juste en
# dessous : la signature dit QUI a poussé, elle ne dit pas CE QUE
# l'image contient. Sur un projet que des opérateurs tiers installent,
# c'est la moitié qui manquait — un `syft`/`grype` sur l'image publiée
# n'avait rien à lire, et un avis de sécurité sur une dépendance
# transitive ne se recoupait pas avec les versions réellement
# embarquées.
sbom: true
provenance: mode=max

- name: Sign the tracker image
env:
Expand Down Expand Up @@ -226,6 +248,17 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64
# Un SBOM et une attestation de provenance, attachés à l'image.
#
# Les images sont déjà signées par cosign en mode sans clé juste en
# dessous : la signature dit QUI a poussé, elle ne dit pas CE QUE
# l'image contient. Sur un projet que des opérateurs tiers installent,
# c'est la moitié qui manquait — un `syft`/`grype` sur l'image publiée
# n'avait rien à lire, et un avis de sécurité sur une dépendance
# transitive ne se recoupait pas avec les versions réellement
# embarquées.
sbom: true
provenance: mode=max

- name: Sign the relay image
env:
Expand Down Expand Up @@ -281,6 +314,17 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64
# Un SBOM et une attestation de provenance, attachés à l'image.
#
# Les images sont déjà signées par cosign en mode sans clé juste en
# dessous : la signature dit QUI a poussé, elle ne dit pas CE QUE
# l'image contient. Sur un projet que des opérateurs tiers installent,
# c'est la moitié qui manquait — un `syft`/`grype` sur l'image publiée
# n'avait rien à lire, et un avis de sécurité sur une dépendance
# transitive ne se recoupait pas avec les versions réellement
# embarquées.
sbom: true
provenance: mode=max

- name: Sign the front-ssr image
env:
Expand Down Expand Up @@ -335,6 +379,17 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64
# Un SBOM et une attestation de provenance, attachés à l'image.
#
# Les images sont déjà signées par cosign en mode sans clé juste en
# dessous : la signature dit QUI a poussé, elle ne dit pas CE QUE
# l'image contient. Sur un projet que des opérateurs tiers installent,
# c'est la moitié qui manquait — un `syft`/`grype` sur l'image publiée
# n'avait rien à lire, et un avis de sécurité sur une dépendance
# transitive ne se recoupait pas avec les versions réellement
# embarquées.
sbom: true
provenance: mode=max

- name: Sign the front image
env:
Expand Down Expand Up @@ -397,6 +452,17 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64
# Un SBOM et une attestation de provenance, attachés à l'image.
#
# Les images sont déjà signées par cosign en mode sans clé juste en
# dessous : la signature dit QUI a poussé, elle ne dit pas CE QUE
# l'image contient. Sur un projet que des opérateurs tiers installent,
# c'est la moitié qui manquait — un `syft`/`grype` sur l'image publiée
# n'avait rien à lire, et un avis de sécurité sur une dépendance
# transitive ne se recoupait pas avec les versions réellement
# embarquées.
sbom: true
provenance: mode=max
# Default base path is `/`, matching the standalone
# docker-run usage. The GitHub Pages deploy
# (deploy-docs.yml) keeps its own `/opentracker/` base
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/docs-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,13 @@ jobs:
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20
# Node 24, comme `doc/Dockerfile` et comme les autres workflows.
# Ce travail était resté sur 20, en fin de vie depuis avril 2026 :
# la même documentation était donc construite sur deux runtimes,
# et le déploiement — qui a `pages: write` — tournait sur celui
# qui ne reçoit plus de correctif. Vérifié : `npm ci` puis
# `npm run build` passent en 24 (vitepress 1.6.4 demande >= 20).
node-version: 24
cache: 'npm'
cache-dependency-path: './doc/package-lock.json'

Expand Down
128 changes: 128 additions & 0 deletions .github/workflows/relay-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Go relay CI — vet, tests, and vulnerability scanning.
#
# The relay had NO workflow at all. It is a publicly reachable HTTP service
# that verifies HMAC-signed bearer tokens and fans out SSE to every connected
# member: nothing enforced that it still compiled, that its tests passed, or
# that it was free of known vulnerabilities. `go build` inside
# apps/relay/Dockerfile is not a gate — it runs after a release is already
# published.
#
# That gap had a visible cost: `apps/relay/go.mod` sat on
# `golang.org/x/sys v0.30.0` long after the tracker had moved to v0.47.0,
# because no job ever ran `go mod tidy` or a vulnerability scan against it.
#
# Deliberately a copy of `tracker-ci.yml` rather than a shared reusable
# workflow: the two modules pin different Go minors over time and have
# different test shapes, and one file per module is what makes a red job point
# straight at its module.
name: Relay CI

on:
push:
branches: [main]
paths:
- 'apps/relay/**'
- '.github/workflows/relay-ci.yml'
pull_request:
paths:
- 'apps/relay/**'
- '.github/workflows/relay-ci.yml'
# Advisories are published against code that hasn't changed, so the vuln
# scan can't only run on pushes. Monday morning, before anyone starts —
# staggered off the tracker's slot so the two don't queue against each other.
schedule:
- cron: '37 6 * * 1'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: relay-ci-${{ github.ref }}
cancel-in-progress: true

defaults:
run:
working-directory: apps/relay

jobs:
test:
name: vet + test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Test-only checkout — nothing is pushed back.
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
# The minor, not `go-version-file`. The `go` directive in go.mod is a
# minimum language version (`go 1.26.0`) and setup-go treats a full
# x.y.z as an exact pin — which on the tracker built CI on go1.26.0
# and failed govulncheck on 19 stdlib vulnerabilities fixed in
# 1.26.1. Tracking the minor matches `golang:1.26-alpine` in the
# Dockerfile. Bump alongside the Dockerfile and the go.mod directive.
go-version: '1.26'
check-latest: true
cache-dependency-path: apps/relay/go.sum

# `gofmt -l` exits 0 even when it lists files, so the emptiness of its
# output is the assertion. `set -e` covers the other failure mode: on a
# file that does not parse, gofmt writes to stderr and exits non-zero
# while printing nothing on stdout.
- name: gofmt
run: |
set -euo pipefail
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "::error::gofmt would rewrite these files — run 'gofmt -w .' in apps/relay"
echo "$unformatted"
gofmt -d .
exit 1
fi

- name: go vet
run: go vet ./...

# -race is the point for this module: the hub subscribes and
# unsubscribes Redis channels from concurrent connection handlers while
# dispatching to a shared map of subscribers.
- name: go test -race
run: go test -race ./...

# `go.mod` drifting behind what the build actually resolves is how the
# x/sys gap survived. `git diff --exit-code` fails the job if tidy moves
# anything.
- name: go mod tidy is up to date
run: |
set -euo pipefail
go mod tidy
git diff --exit-code -- go.mod go.sum

govulncheck:
name: govulncheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Test-only checkout — nothing is pushed back.
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version: '1.26'
check-latest: true
cache-dependency-path: apps/relay/go.sum

- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest

# The check Dependabot cannot do. Dependabot matches advisories against
# declared version ranges; govulncheck walks the call graph and only
# reports a vulnerability when a vulnerable symbol is actually reachable.
# It exits non-zero on a finding, which is what fails the job.
- name: govulncheck
run: govulncheck ./...
Loading
Loading