Some checks are pending
Veza CI / Backend (Go) (push) Waiting to run
Veza CI / Frontend (Web) (push) Waiting to run
Veza CI / Rust (Stream Server) (push) Waiting to run
Veza CI / Notify on failure (push) Blocked by required conditions
E2E Playwright / e2e (full) (push) Waiting to run
Security Scan / Secret Scanning (gitleaks) (push) Waiting to run
The soft-launch report doc (SOFT_LAUNCH_BETA_2026.md) had the
narrative — cohort table, email body inline, monitoring list,
acceptance gate. But the operational pieces were notes-to-self :
"add migration if missing", "Typeform to-do", "schema TBD". The
operator was supposed to assemble them on the day, which on a soft-
launch day is the worst possible time.
Added the missing 6 pieces so the day-of work is "tick boxes",
not "build the tooling" :
* migrations/990_beta_invites.sql — schema with code (16-char
base32-ish), email, cohort label, used_at, expires_at + 30d
default, sent_by FK with ON DELETE SET NULL. Three indexes :
unique on code (signup-path lookup), cohort (post-launch
attribution report), partial expires_at WHERE used_at IS NULL
(cleanup cron).
* scripts/soft-launch/validate-cohort.sh — sanity check on the
operator's CSV : header form, malformed emails, duplicates,
cohort distribution (≥50 total / ≥5 creators / ≥3 distinct
labels), optional collision check against existing users.
Exit codes 0 / 1 (block) / 2 (warn-but-proceed). Hard checks
block, soft checks let the operator override with FORCE=1.
* scripts/soft-launch/send-invitations.sh — split-phase :
step 1 (default) inserts beta_invites rows + renders one .eml
per recipient under scripts/soft-launch/out-<date>/
step 2 (SEND=1) dispatches via $SEND_CMD (msmtp by default)
so the operator can review the rendered emls before sending
100 emails. Per-recipient transactional INSERT so a partial
failure doesn't poison the table. Failed inserts logged with
the offending email so the operator can rerun on the subset.
* templates/email/beta_invite.eml.template — proper MIME multipart
(text + HTML) eml ready for sendmail-compatible piping. French
copy aligned with the éthique brand (no FOMO, no urgency
manipulation, no "limited spots" framing).
* scripts/soft-launch/monitor-checks.sh — polls the 6 acceptance-
gate signals defined in SOFT_LAUNCH_BETA_2026.md §"Acceptance
gate" : testers signed up, Sentry P1 events, status page,
synthetic parcours, k6 nightly age, HIGH issues. Each gate
independently emits ✅ / 🔴 / ⚪ (last for "couldn't check").
Verdict on stdout. LOOP=1 keeps polling every CHECK_INTERVAL
seconds. Designed for cron + tmux, not for an interactive UI.
* docs/SOFT_LAUNCH_BETA_2026_CHECKLIST.md — pre-flight gate that
must reach 100% green before the first invitation goes out.
T-72h section (database, cohort, email infra, redemption path,
monitoring, comms), D-day section (last-hour, send, hour-1,
every-4h), 18:00 UTC decision call section. Linked back to the
bigger SOFT_LAUNCH_BETA_2026.md so the operator can navigate
between the "what" (report) and the "how / has-everything-
been-checked" (this checklist) without losing context.
What still requires the operator on the day :
- Build the cohort CSV (curate emails from real sources)
- Create the Typeform feedback form ; paste its URL into the
eml template once known
- Configure msmtp / sendmail ($SEND_CMD)
- Press the send button
- Show up at 18:00 UTC for the decision call
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
179 lines
6.3 KiB
Bash
Executable file
179 lines
6.3 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# send-invitations.sh — batch-insert beta invitations from a validated
|
|
# cohort CSV, generate unique invite codes, render personalised email
|
|
# bodies, optionally dispatch via SMTP.
|
|
#
|
|
# Wraps the validate-cohort.sh sanity check + a transactional INSERT
|
|
# into beta_invites + a per-recipient email render. Splits "generate
|
|
# the codes + render the emails" from "actually send" so a dry-run
|
|
# produces a flat directory of `.eml` files the operator can review
|
|
# before dispatch.
|
|
#
|
|
# v1.0.10 Cluster 3.4.
|
|
#
|
|
# Usage :
|
|
# # Step 1 : dry-run (default). Inserts beta_invites rows, emits
|
|
# # eml files but does NOT send anything.
|
|
# DATABASE_URL=postgres://... \
|
|
# bash scripts/soft-launch/send-invitations.sh path/to/cohort.csv
|
|
#
|
|
# # Step 2 : after reviewing the eml files, dispatch with msmtp /
|
|
# # sendmail / aws-ses-cli (or whatever SEND_CMD points at).
|
|
# SEND=1 SEND_CMD='msmtp -t' \
|
|
# bash scripts/soft-launch/send-invitations.sh path/to/cohort.csv
|
|
#
|
|
# Required env :
|
|
# DATABASE_URL Postgres URL (read+write to beta_invites)
|
|
# FRONTEND_URL base URL the invite link points at
|
|
# (e.g. https://staging.veza.fr)
|
|
#
|
|
# Optional env :
|
|
# SEND=1 actually dispatch ; otherwise dry-run (eml only)
|
|
# SEND_CMD sendmail-compatible command (default: 'msmtp -t')
|
|
# SENT_BY_EMAIL operator email for the beta_invites.sent_by FK ;
|
|
# defaults to the value in the CSV's third column
|
|
# FROM_ADDR From: header (default: invitations@veza.fr)
|
|
# SUBJECT email subject (default: 'You're in for the Veza beta')
|
|
# TEMPLATE path to eml template (default:
|
|
# templates/email/beta_invite.html.template)
|
|
# FORCE=1 skip validate-cohort.sh failures (use with care)
|
|
#
|
|
# Exit codes :
|
|
# 0 — everything succeeded
|
|
# 1 — cohort validation failed (see validate-cohort.sh)
|
|
# 2 — DB transaction failed
|
|
# 3 — required env missing
|
|
# 4 — dispatch failed for at least one recipient (see logs)
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
|
|
CSV=${1:-}
|
|
if [ -z "$CSV" ] || [ ! -f "$CSV" ]; then
|
|
echo "usage: bash scripts/soft-launch/send-invitations.sh path/to/cohort.csv" >&2
|
|
exit 3
|
|
fi
|
|
|
|
DATABASE_URL=${DATABASE_URL:-?}
|
|
FRONTEND_URL=${FRONTEND_URL:-?}
|
|
[ "$DATABASE_URL" = "?" ] && { echo "DATABASE_URL required" >&2; exit 3; }
|
|
[ "$FRONTEND_URL" = "?" ] && { echo "FRONTEND_URL required" >&2; exit 3; }
|
|
|
|
SEND=${SEND:-0}
|
|
SEND_CMD=${SEND_CMD:-msmtp -t}
|
|
FROM_ADDR=${FROM_ADDR:-invitations@veza.fr}
|
|
SUBJECT=${SUBJECT:-Vous êtes admis dans la bêta Veza}
|
|
TEMPLATE=${TEMPLATE:-$REPO_ROOT/templates/email/beta_invite.eml.template}
|
|
FORCE=${FORCE:-0}
|
|
SESSION_DATE="$(date +%Y%m%d-%H%M)"
|
|
OUTDIR="$REPO_ROOT/scripts/soft-launch/out-${SESSION_DATE}"
|
|
|
|
command -v psql >/dev/null 2>&1 || { echo "psql required" >&2; exit 3; }
|
|
command -v openssl >/dev/null 2>&1 || { echo "openssl required" >&2; exit 3; }
|
|
|
|
# Step 1 — validate the cohort. Bypass with FORCE=1 if needed.
|
|
echo "→ validating cohort $CSV"
|
|
if ! bash "$(dirname "$0")/validate-cohort.sh" "$CSV"; then
|
|
if [ "$FORCE" != "1" ]; then
|
|
echo "ERROR: cohort validation failed. Re-run with FORCE=1 to bypass (not recommended)." >&2
|
|
exit 1
|
|
fi
|
|
echo "WARN : cohort validation reported issues but FORCE=1 set — proceeding."
|
|
fi
|
|
|
|
mkdir -p "$OUTDIR"
|
|
echo "→ output dir $OUTDIR"
|
|
|
|
# Step 2 — generate codes + insert rows + render emails. Each insert
|
|
# is one transaction so a partial failure leaves consistent state.
|
|
gen_code() {
|
|
# 16-char base32-ish (no 0/1/I/L) so codes are paste-friendly.
|
|
openssl rand -hex 16 | tr 'a-f0-9' 'a-z2-9' \
|
|
| tr -d 'oilOIL01' | head -c 16
|
|
}
|
|
|
|
if [ ! -f "$TEMPLATE" ]; then
|
|
echo "ERROR: template $TEMPLATE not found." >&2
|
|
exit 3
|
|
fi
|
|
|
|
inserted=0
|
|
failed=0
|
|
failed_emails=()
|
|
|
|
while IFS=, read -r email cohort sent_by_email; do
|
|
email=$(echo "$email" | tr -d '\r' | xargs)
|
|
cohort=$(echo "$cohort" | tr -d '\r' | xargs)
|
|
sent_by_email=$(echo "$sent_by_email" | tr -d '\r' | xargs)
|
|
|
|
code=$(gen_code)
|
|
|
|
# Resolve sent_by user_id (may be NULL if operator email isn't a
|
|
# registered user — e.g. ops shared mailbox).
|
|
sent_by_id=$(psql "$DATABASE_URL" -A -t -c "
|
|
SELECT id::text FROM users WHERE email = '$sent_by_email' LIMIT 1;
|
|
" 2>/dev/null | tr -d ' ' || echo "")
|
|
|
|
if [ -z "$sent_by_id" ]; then
|
|
sent_by_clause="NULL"
|
|
else
|
|
sent_by_clause="'$sent_by_id'"
|
|
fi
|
|
|
|
if ! psql "$DATABASE_URL" -1 -c "
|
|
INSERT INTO beta_invites (code, email, cohort, sent_by, expires_at)
|
|
VALUES ('$code', '$email', '$cohort', $sent_by_clause, NOW() + INTERVAL '30 days');
|
|
" >/dev/null 2>&1; then
|
|
failed=$(( failed + 1 ))
|
|
failed_emails+=("$email")
|
|
continue
|
|
fi
|
|
inserted=$(( inserted + 1 ))
|
|
|
|
# Render the eml — operator-readable, ready for SEND_CMD.
|
|
eml="$OUTDIR/${email//[^a-zA-Z0-9._-]/_}.eml"
|
|
invite_url="$FRONTEND_URL/signup?invite=$code"
|
|
sed \
|
|
-e "s|{{TO_ADDR}}|$email|g" \
|
|
-e "s|{{FROM_ADDR}}|$FROM_ADDR|g" \
|
|
-e "s|{{SUBJECT}}|$SUBJECT|g" \
|
|
-e "s|{{INVITE_URL}}|$invite_url|g" \
|
|
-e "s|{{INVITE_CODE}}|$code|g" \
|
|
-e "s|{{COHORT}}|$cohort|g" \
|
|
-e "s|{{FRONTEND_URL}}|$FRONTEND_URL|g" \
|
|
"$TEMPLATE" > "$eml"
|
|
done < <(tail -n +2 "$CSV")
|
|
|
|
echo "→ inserted $inserted invitations into beta_invites"
|
|
echo "→ rendered $inserted emails to $OUTDIR"
|
|
[ "$failed" -gt 0 ] && {
|
|
echo "WARN : $failed insert(s) failed — see logs above"
|
|
for e in "${failed_emails[@]}"; do echo " - $e"; done
|
|
}
|
|
|
|
# Step 3 — optionally dispatch.
|
|
if [ "$SEND" != "1" ]; then
|
|
echo ""
|
|
echo "DRY-RUN — review the eml files in $OUTDIR before sending."
|
|
echo "When ready :"
|
|
echo " SEND=1 SEND_CMD='$SEND_CMD' bash scripts/soft-launch/send-invitations.sh $CSV"
|
|
exit 0
|
|
fi
|
|
|
|
echo "→ dispatching via : $SEND_CMD"
|
|
dispatch_failed=0
|
|
for eml in "$OUTDIR"/*.eml; do
|
|
if ! $SEND_CMD < "$eml" >>"$OUTDIR/dispatch.log" 2>&1; then
|
|
dispatch_failed=$(( dispatch_failed + 1 ))
|
|
echo " FAIL : $eml" | tee -a "$OUTDIR/dispatch.log"
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
if [ "$dispatch_failed" -gt 0 ]; then
|
|
echo "WARN : $dispatch_failed dispatch(es) failed — see $OUTDIR/dispatch.log"
|
|
exit 4
|
|
fi
|
|
echo "PASS : all $inserted invitations dispatched."
|
|
echo "Track redemption with :"
|
|
echo " psql \"\$DATABASE_URL\" -c 'SELECT cohort, count(*) FILTER (WHERE used_at IS NOT NULL) AS redeemed, count(*) AS total FROM beta_invites GROUP BY cohort ORDER BY cohort;'"
|