Chapitre 14Mettre en ligne
GitHub, de bout en bout
Dépôt, compte machine, jeton à portée restreinte, variables, intégration continue, déploiement, vérifications. Le chemin complet, sans étape sous-entendue.
Deux rôles à ne pas confondre
GitHub sert ici à deux choses différentes, et il est utile de les séparer dans sa tête :
Rôle 1
Héberger le code et déclencher les builds
Le dépôt du site, l'intégration continue, et la connexion à l'hébergeur qui reconstruit à chaque poussée.
Rôle 2
Servir de base de données de contenu
La fonction /api/save écrit les fichiers JSON par l'API Contents, avec le jeton de l'agence. C'est là qu'intervient GIT_TOKEN.
Le second rôle est le seul qui demande un jeton. Un site peut parfaitement être déployé depuis GitHub sans que la fonction n'y écrive — mais alors il n'est pas éditable.
1. Le dépôt
-
Un dépôt par client
Le contenu d'un client ne doit jamais côtoyer celui d'un autre : c'est aussi ce qui circonscrit un incident. Nom explicite :
agence/boulangerie-martin. -
Privé
Le contenu est public sur le site, mais le dépôt contient l'historique, les adresses et la configuration. Rien n'oblige à l'exposer.
-
Une branche de publication
mainpar défaut. C'est elle que déclareGIT_BRANCH, et c'est elle que l'hébergeur reconstruit. -
Premier envoi
bash bash cd boulangerie-martin git init -b main git add . git commit -m "Mise en place du site" git remote add origin git@github.com:agence/boulangerie-martin.git git push -u origin mainVérifiez que
.dev.varsn'est pas parti : il est dans.gitignore, mais ungit add -fmalheureux arrive.
Si vous protégez main en exigeant une revue ou un contrôle réussi avant fusion,
les publications du client seront rejetées : la fonction écrit directement
sur la branche. Deux options — ne pas exiger de revue sur cette branche, ou autoriser
explicitement le compte machine à contourner la règle. Testez-le avant la livraison, pas
après.
2. Le compte machine
Le jeton d'écriture ne doit appartenir à personne. S'il appartient à un développeur, il part avec lui, et son périmètre est celui de tous ses dépôts.
- Créer un compte GitHub dédié à l'agence (par exemple
agence-bot), avec une adresse de courriel de service et l'authentification à deux facteurs. - L'inviter sur le dépôt du client avec le rôle Write, et rien de plus.
- C'est depuis ce compte que le jeton est créé.
En organisation, on peut aussi utiliser un jeton d'organisation à portée restreinte : le principe reste le même — une identité qui n'est pas une personne, limitée à un dépôt.
3. Le jeton à portée restreinte
Depuis le compte machine :
-
Settings → Developer settings → Personal access tokens → Fine-grained tokens
-
Generate new token
- Resource owner : le compte ou l'organisation propriétaire du dépôt.
- Repository access : Only select repositories → le dépôt du client, uniquement.
- Repository permissions : Contents → Read and write. Tout le reste sur No access.
- Expiration : la plus courte compatible avec votre exploitation.
-
Copier le jeton
Il n'est affiché qu'une fois. Il va chez l'hébergeur, jamais dans le dépôt, jamais dans un courriel.
-
Noter la date d'expiration
Un jeton expiré ne casse pas le site — il casse la publication, et le client découvre le problème en essayant de modifier une page. Posez un rappel une semaine avant.
La fonction ne fait que deux choses : lire un fichier et en écrire un. Elle n'a besoin ni des tickets, ni des actions, ni des membres, ni des paramètres du dépôt. Un jeton plus large n'apporte rien et coûte tout le jour où il fuit.
4. Les variables
GIT_PROVIDER=github
GIT_REPO=agence/boulangerie-martin
GIT_BRANCH=main
GIT_TOKEN=github_pat_…
EDITOR_KEY_HASH=$argon2id$v=19$m=19456,t=2,p=1$…
SESSION_SECRET=…
EDITOR_NAME=Éditeur du site
EDITOR_EMAIL=contact@boulangerie-martin.fr
GIT_REPO s'écrit propriétaire/dépôt, sans https:// ni
.git. EDITOR_NAME et EDITOR_EMAIL deviennent l'auteur
des commits : c'est ce que le client verra dans l'historique, alors autant que ce soit lisible.
5. Ce que la fonction appelle
Deux points d'entrée de l'API Contents, et deux seulement. Utile à connaître pour lire un journal d'erreur.
| Opération | Appel | Ce qui en est tiré |
|---|---|---|
| Lecture | GET /repos/{repo}/contents/{chemin}?ref={branche} |
Le contenu (base64) et le SHA du blob, qui sert de version au verrou optimiste. |
| Écriture | PUT /repos/{repo}/contents/{chemin} |
Message, contenu, sha attendu, branche, auteur. GitHub refuse lui-même un sha périmé. |
Le verrou optimiste est donc appliqué deux fois : par la fonction, qui compare la
version avant d'écrire, et par GitHub, qui rejette un sha qui ne correspond plus.
| Réponse GitHub | Traduite en | Vu par le client |
|---|---|---|
404 | not_found | « Cette modification n'a pas pu être enregistrée » |
401 / 403 | unauthorized | idem |
409 / 422 | conflict | « Quelqu'un d'autre a publié pendant que vous travailliez » |
| autre | unavailable | « Cette modification n'a pas pu être enregistrée » |
6. Éprouver le fournisseur, avant tout le reste
# Hors ligne : la mécanique, sans réseau
npm run test:git
# Lecture réelle sur un vrai dépôt
GIT_REPO=agence/boulangerie-martin GIT_TOKEN=github_pat_… \
node scripts/test-git-provider.mjs --online
# Écriture réelle + conflit provoqué (écrit un fichier d'essai dans le dépôt)
GIT_REPO=agence/boulangerie-martin GIT_TOKEN=github_pat_… \
node scripts/test-git-provider.mjs --online --writeC'est le moyen le plus rapide de savoir si un jeton a les bons droits : quinze secondes contre un déploiement complet.
7. L'intégration continue
Le workflow livré lance exactement les commandes qu'on lance avant de commiter, et il bloque la branche.
name: Contrôles
on:
push:
branches: [main]
pull_request:
# Une poussée qui en remplace une autre annule la précédente.
concurrency:
group: controles-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
verifier:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm test # construit le site au passage
- run: npm run check # HTML brut, langues, journaux, secrets
Les contrôles ne touchent ni au dépôt de contenu ni au jeton d'écriture : ils construisent le
site et l'inspectent. permissions: contents: read le dit explicitement.
Le dépôt de référence ajoute un second travail, échafaudage, qui crée un site de zéro,
l'installe, le construit et le contrôle. Sans lui, le modèle embarqué par
create-inline diverge en silence, et on ne s'en aperçoit qu'au prochain client.
8. Le déploiement
Le site est statique et les fonctions vivent dans /functions. Deux façons de le
mettre en ligne depuis GitHub.
A. Connecter le dépôt à l'hébergeur (recommandé)
-
Créer le projet chez l'hébergeur, branché sur le dépôt GitHub
Commande de build :
npm run build. Dossier publié :dist. -
Poser les variables en secrets d'exécution
Pas en variables de build : elles ne doivent jamais atteindre le navigateur.
-
Déclarer la liaison clé-valeur
RATE_LIMIT -
Vérifier
Voir Déploiement pour la liste complète.
Chaque publication du client produit un commit sur main ; l'hébergeur le détecte
et reconstruit. C'est la boucle complète, et elle ne demande aucun webhook à écrire.
B. Déployer depuis GitHub Actions
Utile si vous voulez maîtriser l'ordre : contrôles d'abord, déploiement ensuite. Le jeton d'écriture du contenu n'a rien à faire ici — seuls les identifiants de l'hébergeur sont nécessaires.
name: Déploiement
on:
push:
branches: [main]
permissions:
contents: read
jobs:
deployer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run build
- run: npm run check
- name: Publier
run: npx wrangler pages deploy dist --project-name=boulangerie-martin
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
GitHub Pages sert des fichiers statiques, et n'exécute pas de fonctions. Le
site s'afficherait parfaitement et /api/* répondrait 404 : aucune édition
possible. Il faut un hébergeur capable d'exécuter /functions.
9. Vérifications après mise en ligne
curl -s https://le-site.fr/ | grep -c "un titre de la page"→ 1curl -s -X POST https://le-site.fr/api/save→ 401- Cinq clés fausses de suite sur
/admin→ message d'attente. - Une modification publiée depuis
/adminapparaît en commit dans le dépôt, attribuée àEDITOR_NAME. - Le site est reconstruit dans la minute qui suit ce commit.
- Le message de commit ressemble à
content(fr): home — hero.title.
Quand ça ne marche pas
| Symptôme | Cause la plus fréquente |
|---|---|
Publication refusée, journaux : not_found | GIT_REPO mal écrit, branche inexistante, ou jeton sans accès à ce dépôt. |
Journaux : unauthorized | Jeton expiré, révoqué, ou permission Contents absente. |
Journaux : conflict systématique | Le fichier change entre la lecture et l'écriture — un autre processus écrit dans le dépôt. |
| Le commit arrive, le site ne change pas | L'hébergeur n'écoute pas cette branche, ou le build échoue. Regarder le journal de build. |
| Le commit est rejeté | Protection de branche : revue obligatoire ou contrôle requis sur main. |
| Tout marchait, plus rien ne marche | Expiration du jeton. C'est la panne la plus banale de ce montage. |
Un compte authentifié dispose de 5 000 appels par heure sur l'API GitHub. Une session d'édition en consomme quelques dizaines. Les budgets de débit des routes sont dimensionnés bien en dessous : le quota n'est pas un sujet en usage normal, et il le devient si un script part en boucle — d'où les budgets.