L'origine du projet
Suivre les actualités Swift.org demandait de consulter régulièrement leur site. J'ai vu une opportunité : créer un bot Discord qui notifie automatiquement ma communauté des nouveaux articles.
Ce qui devait être un simple script s'est transformé en un véritable projet production-ready, avec CI/CD, Docker, monitoring et bonnes pratiques DevOps. Voici le parcours de développement et les décisions techniques associées.
En résumé : EchoSwift est un bot Discord open-source qui poste automatiquement les articles Swift.org, déployé avec GitHub Actions et Docker.
Le besoin
La communauté Swift évolue vite : annonces importantes, améliorations du langage, retours d'expérience sur le blog officiel. Comment rester à jour sans consulter le site plusieurs fois par jour ?
La réponse : un bot Discord qui vérifie automatiquement le feed Swift.org (midi et minuit), détecte les nouveaux articles, les poste dans un format soigné avec notification de rôle, et évite les doublons via un système de tracking.
Choix de la stack technique
Bun comme runtime
Au lieu de Node.js, j'ai opté pour Bun pour plusieurs raisons.
Performance : démarrage quasi instantané (contre ~500ms pour Node.js), consommation mémoire réduite (~30 MB contre ~50 MB), bundler et test runner intégrés.
bun install # 10-20x plus rapide que npm
bun run dev # Hot reload natif
bun test # Test runner ultra-rapideTypeScript natif : pas besoin de ts-node ou de compilation, Bun exécute directement le TypeScript.
Architecture applicative
src/
├── index.ts # Point d'entrée, initialisation
├── config.ts # Configuration et validation env
├── bot.ts # Client Discord et event handlers
├── scheduler.ts # Cron jobs et orchestration
├── feed/
│ ├── fetcher.ts # Client HTTP avec retry
│ ├── parser.ts # Parsing Atom/RSS
│ └── storage.ts # Persistence JSON avec écritures atomiques
└── discord/
├── embeds.ts # Création des Discord embeds
└── poster.ts # Posting avec rate limiting
Chaque module a une responsabilité unique, ce qui facilite les tests et la maintenance.
Défis techniques et solutions
Scheduler intelligent avec node-cron
Le défi : exécuter le bot à heures fixes dans le bon fuseau horaire.
import cron from 'node-cron';
const cronPattern = '0 12,0 * * *'; // Midi et minuit
const task = cron.schedule(
cronPattern,
() => checkFeedAndPost(client, config),
{ timezone: config.timezone } // 'Europe/Paris'
);En développement, exécution immédiate pour faciliter les tests. En production, attente du cron pour éviter les posts intempestifs.
Déduplication et stockage atomique
Le défi : ne pas reposter les mêmes articles après un redémarrage.
async function savePostedArticles(articleIds: Set<string>): Promise<void> {
const tempPath = STORAGE_PATH + '.tmp';
const backupPath = STORAGE_PATH + '.backup';
// 1. Backup de l'existant
if (existsSync(STORAGE_PATH)) {
await copyFile(STORAGE_PATH, backupPath);
}
// 2. Écriture dans un fichier temporaire
await writeFile(tempPath, JSON.stringify(data, null, 2));
// 3. Renommage atomique (opération système atomique)
await rename(tempPath, STORAGE_PATH);
}Un nettoyage automatique supprime les entrées de plus de 30 jours pour éviter une croissance sans fin.
Support des salons d'annonce Discord
Le bot crashait initialement avec les salons d'annonce (GuildAnnouncement). La solution : élargir la vérification des types de canaux.
// Avant (restrictif)
if (channel.type !== ChannelType.GuildText) {
throw new Error('Not a text channel');
}
// Après (flexible)
if (channel.type !== ChannelType.GuildText &&
channel.type !== ChannelType.GuildAnnouncement) {
throw new Error('Not a text or announcement channel');
}Rate limiting et posting robuste
Discord limite à environ 5 messages par 5 secondes. La solution : une attente de 2 secondes entre chaque post et une gestion d'erreur qui continue avec les articles suivants en cas de problème.
for (let i = 0; i < articles.length; i++) {
try {
await postArticle(client, channelId, roleId, articles[i]);
if (i < articles.length - 1) {
await new Promise(resolve => setTimeout(resolve, 2000));
}
} catch (error) {
console.error(`Error posting article ${i + 1}:`, error);
}
}Embeds Discord soignés
Les articles sont affichés avec des embeds propres : description courte (200 caractères max), titre cliquable, couleur Swift orange, et timestamp de publication.
return new EmbedBuilder()
.setTitle(article.title)
.setURL(article.link)
.setDescription(description)
.setColor(0xF05138)
.setFooter({ text: `swift.org - Published ${dateStr}` })
.setTimestamp(article.publishedAt);DevOps : du code au déploiement
Docker multi-stage build
L'objectif : une image légère et sécurisée.
# Stage 1: Build
FROM oven/bun:1-alpine AS builder
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY src ./src
COPY tsconfig.json ./
# Stage 2: Production
FROM oven/bun:1-alpine
WORKDIR /app
RUN addgroup -g 1001 -S botuser && \
adduser -S -D -H -u 1001 -G botuser botuser
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
COPY --from=builder /app/src ./src
COPY --from=builder /app/tsconfig.json ./
RUN mkdir -p /app/data && chown -R botuser:botuser /app/data
VOLUME ["/app/data"]
USER botuser
CMD ["bun", "run", "src/index.ts"]L'image finale pèse environ 100 MB (contre 300+ MB avec Node.js), avec sécurité renforcée (utilisateur non-root) et build reproductible.
CI/CD avec GitHub Actions
Le workflow complet : tests automatiques avec Bun, build et push de l'image Docker vers GHCR, déploiement SSH automatique sur VPS, et redémarrage du container.
name: Build & Deploy
on:
push:
branches: [main]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
- name: Run tests
run: bun test
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:latest
deploy:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Deploy to VPS
uses: appleboy/[email protected]
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /opt/echoswift
sudo docker compose pull
sudo docker compose up -d --force-recreateBenchmarks
- Mémoire : Bun ~30 MB vs Node.js ~50 MB (40% d'économie)
- Démarrage : Bun ~50ms vs Node.js ~500ms (10x plus rapide)
- Cold start Docker : Bun ~800ms vs Node.js ~2s (2.5x plus rapide)
Retours d'expérience
Ce qui a bien fonctionné : Bun (excellent pour un projet léger et performant), les écritures atomiques (prévention de la corruption de données), le CI/CD dès le début (itérations rapides), les multi-stage builds (images légères et sécurisées).
Ce que je ferais différemment : ajouter des tests d'intégration avec un mock Discord, intégrer Sentry ou Datadog pour le monitoring, mettre en place des health checks HTTP, collecter des métriques (articles postés, temps de réponse), et utiliser PostgreSQL au lieu de JSON pour la scalabilité.
EchoSwift est bien plus qu'un simple bot Discord : c'est une démonstration complète de comment construire, tester et déployer une application production-ready avec des technologies modernes.

Commentaires
Aucun commentaire pour le moment. Sois le premier !