De la chaîne Jekyll au Draft PR Astro : séparer candidat et publication
Un générateur de contenu doit produire un candidat à examiner. Dans ce dépôt, une génération et une CI réussies conduisent à un Draft PR ; la publication suit une fusion distincte et le workflow Pages de la branche principale.
La chaîne Jekyll d’avril écrivait du Markdown et poussait le contenu généré. Le site utilise maintenant Astro, stocke les nouveaux digests bilingues en JSON et garde le candidat hors de main jusqu’à sa revue. Les fichiers _posts et les outils Ruby existants décrivent l’ancien site, et non la commande de publication actuelle.
Suivre le commit du candidat
Le workflow de contenu ne génère que sur main. Il recherche d’abord un PR ouvert content/digest-review- provenant de ce dépôt. S’il en existe un, il suspend un autre candidat, y compris une exécution manuelle de prévisualisation, jusqu’à la fusion ou la fermeture de ce PR.
Sans candidat en attente, scripts/generate_digest.py collecte et prépare les éditions anglaise et coréenne. Un fichier généré doit passer npm run check et npm run build avant qu’un commit soit poussé sur une nouvelle branche de revue. Le validateur partagé vérifie ensuite le SHA de ce commit généré, et le job de revue dépend de la réussite de la génération et de la validation. Cela suit le comportement des dépendances entre jobs de GitHub.
Le job de revue rend les preuves, crée un Draft PR, ajoute l’URL de prévisualisation et déclenche explicitement le workflow du site sur la branche de revue. Cette branche peut être vérifiée sans satisfaire la condition de déploiement en production. Les règles d’événements des tokens comptent aussi lorsqu’une automatisation déclenche un autre workflow ; ne déduisez pas une exécution d’une seule requête API réussie.
Exercer la logique de revue sans appeler GitHub
Utilisez Node 24 et exécutez npm ci dans une copie de ce dépôt. Enregistrez le fichier complet suivant sous review-contract.mjs à la racine du dépôt, puis exécutez :
node review-contract.mjs
Ce programme de test lit le véritable YAML du workflow, exécute ses deux extraits JavaScript avec des simulations locales et vérifie cinq cas de PR en attente ainsi que la séquence création/mise à jour/déclenchement. Sa simulation n’expose aucune opération de fusion. Il n’exécute pas le YAML du workflow, ne génère pas de vrai digest et ne fait aucun appel API.
review-contract.mjs
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import YAML from 'yaml';
const workflow = YAML.parse(readFileSync('.github/workflows/dev-content-pipeline.yml', 'utf8'));
const { generate, validate, review } = workflow.jobs;
assert.equal(generate.if, "github.ref == 'refs/heads/main'");
assert.deepEqual(review.needs, ['generate', 'validate']);
assert.equal(validate.with.ref, '${{ needs.generate.outputs.commit_sha }}');
assert.equal(validate.uses, './.github/workflows/site-checks.yml');
assert.equal(workflow.jobs.publish, undefined);
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
const pendingScript = generate.steps.find(step => step.id === 'pending').with.script;
const runPending = new AsyncFunction('github', 'context', 'core', pendingScript);
const context = { repo: { owner: 'example', repo: 'blog' } };
const summary = { addRaw() { return this; }, async write() {} };
const cases = [
[[], false],
[[{ head: { repo: { full_name: 'example/blog' }, ref: 'content/digest-review-1' }, html_url: 'mock' }], true],
[[{ head: { repo: { full_name: 'other/blog' }, ref: 'content/digest-review-1' } }], false],
[[{ head: { repo: { full_name: 'example/blog' }, ref: 'content/manual-1' } }], false],
[[{ head: { repo: null, ref: 'content/digest-review-1' } }], false],
];
for (const [pulls, expected] of cases) {
let exists;
const github = { rest: { pulls: { list() {} } }, paginate: async () => pulls };
const core = { summary, setOutput(key, value) { if (key === 'exists') exists = value; } };
await runPending(github, context, core);
assert.equal(exists, expected);
}
const calls = [];
const github = { rest: {
pulls: {
async create(payload) { calls.push(['create', payload]); return { data: { number: 42, html_url: 'mock-pr' } }; },
async update(payload) { calls.push(['update', payload]); },
},
actions: { async createWorkflowDispatch(payload) { calls.push(['dispatch', payload]); } },
} };
const core = { summary, setOutput() {} };
const requireMock = name => {
assert.equal(name, 'node:fs');
return { readFileSync: () => 'Mock review body' };
};
const script = review.steps.find(step => step.id === 'pr').with.script;
const runReview = new AsyncFunction('require', 'github', 'context', 'core', 'process', script);
await runReview(requireMock, github,
{ ...context, serverUrl: 'https://github.com', runId: 7 }, core,
{ env: { REVIEW_BRANCH: 'content/digest-review-7-1' } });
assert.deepEqual(calls.map(call => call[0]), ['create', 'update', 'dispatch']);
assert.equal(calls[0][1].draft, true);
assert.equal(calls[0][1].base, 'main');
assert.equal(calls[0][1].head, 'content/digest-review-7-1');
assert.equal(calls[2][1].workflow_id, 'site-quality-gates.yml');
assert.equal(calls[2][1].ref, calls[0][1].head);
console.log('5 pending-PR cases and the mocked draft/create-update-dispatch sequence passed.');
La sortie vérifiée est :
5 pending-PR cases and the mocked draft/create-update-dispatch sequence passed.
Les cinq entrées distinguent l’absence de PR en attente, un PR digest local correspondant, un PR d’un autre dépôt, une autre branche locale et un dépôt head manquant. La réussite du programme démontre ces contrats de script sélectionnés, et non les permissions du token GitHub, la protection de branche, l’exactitude des sources ou l’ensemble du workflow hébergé.
Demander un candidat et décider de sa publication
Dans Actions → Dev Content Pipeline → Run workflow, sélectionnez main. Laissez preview décoché pour préparer un candidat à examiner. Cochez-le pour le parcours à blanc, qui utilise --dry-run --force --max-items 2 et ne committe pas de candidat. Un digest déjà en attente suspend toujours la génération. GitHub documente les commandes manuelles de workflow.
Ouvrez le Draft PR et sa prévisualisation bilingue. Vérifiez la date de la source, l’affirmation initiale et ses preuves, le périmètre de traduction, les liens et l’intérêt de publier l’élément. Les contrôles automatiques de code et de schéma ne prennent pas cette décision éditoriale. Modifiez le candidat si nécessaire, confirmez les contrôles du commit résultant, puis marquez-le prêt et fusionnez lorsque la publication est approuvée. Marquer simplement un brouillon prêt ne le déploie pas.
L’article sur les contrôles qualité explique pourquoi une exécution PR verte peut légitimement ignorer le déploiement et quoi vérifier après la fusion. Pour l’ancien environnement Ruby, voir le compte rendu de dépannage Jekyll, actuellement disponible en coréen.
Discussion