Concurrency. release.yml calls ci.yml, so a push to main started the
called workflow and the standalone one, both landing in the concurrency
group ci-<ref>. One cancelled the other and every merge left a cancelled
run behind. main is now excluded from the push trigger: release.yml is
already the entry point there, and pull_request still covers changes
before they land.
Node 20 deprecation (23 warnings). Every action was pinned to a major
that still declares node20. Each replacement was checked by reading the
action.yml at the new tag and confirming "using: node24", and the inputs
in use were confirmed to still exist there:
checkout v4->v7, setup-node v4->v7, setup-go v5->v7,
upload-artifact v4->v7, download-artifact v4->v7,
setup-buildx-action v3->v4, build-push-action v6->v7,
login-action v3->v4, metadata-action v5->v6
download-artifact has a v8, but v7 is also node24 and matches the upload
side, so the pair stays on one major. trivy-action is a composite action
and has no Node runtime to deprecate.
Go cache (1 warning). setup-go looked for go.sum at the repository root
and skipped its cache: defaults.run.working-directory applies to run
steps, not to actions. cache-dependency-path now points at
services/protocols/go.sum.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI gains a security job (Trivy fs: vuln, secret and misconfig) and an
image job that builds each of the four services and scans the result. The
secret scanner earns its place here: a DKIM key or Cloudflare token
committed by accident is a full compromise of every domain served.
release.yml publishes on a merge into main and on v* tags. Four services
on two architectures is eight build jobs pushing by digest, then four
publish jobs joining each pair into one manifest list, tagged latest, sha
and semver. github.repository is lowercased first because GHCR rejects an
uppercase path and this repository is lucasrguerra/LambdaMail.
Both scanners failed on first run, so this commit also fixes what they
found rather than shipping a workflow that is red on arrival:
- All four images ran as root (DS-0002), which section 17 risk R5
explicitly argues against. They now run unprivileged; protocols keeps
its eight ports below 1024 through cap_net_bind_service on the binary
rather than through being root.
- A HIGH advisory in postcss, reached only through Next. "npm audit fix
--force" would have resolved it by downgrading Next from 15 to 9, so
it is pinned with an override instead.
- The Node images copied the build stage's node_modules wholesale,
shipping typescript, eslint and vitest into production, and the base
image's own bundled npm accounted for the rest of the CVEs. Runtime
dependencies now come from npm ci --omit=dev and npm is removed from
the final layer.
- compose-up could never have passed: the proxy network is external
because Coolify owns it on the deploy host, and nothing creates it in
CI, so compose refused to start the stack at all.
Verified locally: actionlint clean, all ten action refs confirmed to
exist, Trivy fs and all four image scans exit 0, the full stack reaches
Healthy on every container, and the containers run as uid 10001/1000 with
all eight privileged ports bound and SMTP answering on 25. The arm64
build itself is unverified - this machine has no buildx - and will first
be exercised by release.yml.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a real :25 SMTP listener to the Go protocols service, giving F1's
"Nucleo SMTP" milestone its first working sub-project: application,
infrastructure, and presentation layers on top of F0's domain-only
Clean Architecture.
- ProcessInboundEmailUseCase resolves recipients against Postgres and
persists accepted mail (blob dedup by content hash, transactional
UID allocation + outbox event, never sending 250 OK before durable
commit per PLAN.md section 6.1).
- Postgres repositories via pgx/v5 (replacing lib/pq entirely).
- Local-disk blob storage with content-hash dedup, rename-before-commit
ordering so a DB row can never reference a file that isn't actually
on disk.
- A local-file TLS cert provider for STARTTLS, with an ephemeral
self-signed fallback so a missing/misconfigured cert degrades
gracefully instead of crash-looping the service (PLAN.md section 8.4).
- A github.com/emersion/go-smtp Backend/Session adapting real SMTP
traffic to the use case: unknown recipients get 550 5.1.1 before
DATA, duplicate RCPT TO for the same address is deduplicated, TLS is
hardened to the project's required PFS-only cipher/curve policy.
- A dev-seed script and Makefile target for local testing.
Explicitly out of scope (separate follow-up sub-projects): alias
resolution, SPF/DKIM/DMARC/ARC verification, Rspamd/ClamAV, Submission
ports with SASL auth, DKIM signing, outbound delivery, and the real
Traefik acme.json cert watcher.
Verified live end-to-end via WSL Docker: the full compose stack (db,
redis, migrate, auth, storage, protocols, web) reaches healthy, and a
real swaks SMTP session is accepted and durably persisted with correct
sequential UIDs, while an unknown recipient is correctly rejected with
550 5.1.1 and zero rows persisted. Built via 10 sequential tasks under
subagent-driven development, each independently reviewed, plus a final
whole-branch review that caught and fixed a TLS-crash bug, a blob
commit-ordering bug, missing TLS hardening, and missing CI integration
coverage before merge.
Known follow-up, not blocking: email_messages' partition only covers
July 2026 and needs a partition-management job before 2026-08-01.
Stands up the LambdaMail monorepo per PLAN.md's F0 milestone: local dev
scaffolding (.env.example, Makefile, .gitignore excluding PLAN.md/docs/
.superpowers/), the full initial Postgres schema as a golang-migrate
migration, the complete Go domain layer for services/protocols (value
objects, entities, domain events - zero external imports beyond
github.com/google/uuid) with unit tests, health-checked service skeletons
for auth/storage (TypeScript) and webmail (Next.js), a local
docker-compose.yaml wiring all services together, a language-enforcement
lint script, and a GitHub Actions CI workflow.
Built via 11 sequential tasks under subagent-driven development, each with
an independent implementer + reviewer cycle. Fixes applied during review:
DomainName.IsPunycode() added to satisfy the promised interface; ioredis
default-import typing under NodeNext; next.js bumped 15.0.3->15.5.21 to
resolve high/critical CVEs; lang-lint.mjs corrected to not flag its own
banned-terms list; web service healthcheck added so compose --wait
actually waits on it; webmail Dockerfile's missing public/ directory:
protocols go.mod version aligned with CI's Go 1.22 pin.
Known follow-ups for later phases (not blocking F0): go-arch-lint isn't
wired into CI yet; live docker compose up and migration apply have not
been exercised in this dev environment (no Docker/psql available here)
and will get their first real run in CI, which does have Docker.