FR EN

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.

7 min de lecture13 sectionsChapitre 14 / 22

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

  1. 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.

  2. 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.

  3. Une branche de publication

    main par défaut. C'est elle que déclare GIT_BRANCH, et c'est elle que l'hébergeur reconstruit.

  4. 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 main

    Vérifiez que .dev.vars n'est pas parti : il est dans .gitignore, mais un git add -f malheureux arrive.

Protection de branche : attention

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 :

  1. Settings → Developer settings → Personal access tokens → Fine-grained tokens

  2. 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 : ContentsRead and write. Tout le reste sur No access.
    • Expiration : la plus courte compatible avec votre exploitation.
  3. 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.

  4. 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.

Pourquoi Contents et rien d'autre

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

chez l'hébergeur — en secrets d'exécution env
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érationAppelCe 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 GitHubTraduite enVu par le client
404not_found« Cette modification n'a pas pu être enregistrée »
401 / 403unauthorizedidem
409 / 422conflict« Quelqu'un d'autre a publié pendant que vous travailliez »
autreunavailable« Cette modification n'a pas pu être enregistrée »

6. Éprouver le fournisseur, avant tout le reste

depuis le dépôt de référence bash
# 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 --write

C'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.

.github/workflows/ci.yml yaml
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
Aucun secret n'est nécessaire ici

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é)

  1. Créer le projet chez l'hébergeur, branché sur le dépôt GitHub

    Commande de build : npm run build. Dossier publié : dist.

  2. Poser les variables en secrets d'exécution

    Pas en variables de build : elles ne doivent jamais atteindre le navigateur.

  3. Déclarer la liaison clé-valeur RATE_LIMIT

  4. 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.

.github/workflows/deploy.yml yaml
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 ne convient pas

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" → 1
  • curl -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 /admin apparaî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ômeCause la plus fréquente
Publication refusée, journaux : not_foundGIT_REPO mal écrit, branche inexistante, ou jeton sans accès à ce dépôt.
Journaux : unauthorizedJeton expiré, révoqué, ou permission Contents absente.
Journaux : conflict systématiqueLe fichier change entre la lecture et l'écriture — un autre processus écrit dans le dépôt.
Le commit arrive, le site ne change pasL'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 marcheExpiration du jeton. C'est la panne la plus banale de ce montage.
Quota d'API

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.