От автопубликации Jekyll к проверке Draft PR в Astro
Генератор контента должен создавать кандидата для проверки. В этом репозитории успешная генерация и CI приводят к Draft PR; публикация следует за отдельным слиянием и workflow Pages ветки main.
Апрельный pipeline Jekyll записывал Markdown и отправлял сгенерированный контент. Теперь сайт использует Astro, хранит новые двуязычные дайджесты как JSON и держит кандидата вне main до проверки. Существующие файлы _posts и инструменты Ruby описывают прежний сайт, а не текущую команду публикации.
Проследите коммит кандидата
Workflow контента запускает генерацию только на main. Сначала он ищет открытый PR content/digest-review- из этого репозитория. При его наличии создание следующего кандидата, включая ручной preview, приостанавливается до слияния или закрытия PR.
Если ожидающего кандидата нет, scripts/generate_digest.py собирает и готовит английскую и корейскую версии. Сгенерированный файл должен пройти npm run check и npm run build до отправки коммита в новую ветку проверки. Общий валидатор затем проверяет SHA этого коммита, а задача review зависит от успеха генерации и валидации. Это соответствует зависимостям задач GitHub.
Задача review отображает доказательства, создаёт Draft PR, добавляет URL preview и явно запускает site workflow на ветке проверки. Ветку можно проверить без выполнения условия производственного развёртывания. Правила событий токена тоже важны при запуске другого workflow автоматизацией; успешный API-запрос сам по себе не доказывает наличие запуска.
Проверьте логику review без вызовов GitHub
Используйте Node 24 и выполните npm ci в checkout этого репозитория. Сохраните полный файл как review-contract.mjs в корне, затем выполните:
node review-contract.mjs
Проверка читает настоящий YAML workflow, выполняет два JavaScript-фрагмента с локальными mock и проверяет пять случаев ожидающего PR и последовательность create/update/dispatch. Mock не предоставляет операцию слияния. Она не выполняет YAML workflow, не создаёт настоящий дайджест и не вызывает 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.');
Проверенный вывод:
5 pending-PR cases and the mocked draft/create-update-dispatch sequence passed.
Пять входов различают отсутствие ожидающего PR, подходящий локальный PR дайджеста, PR другого репозитория, другую локальную ветку и отсутствие head-репозитория. Успех подтверждает выбранные контракты скрипта, а не разрешения токена GitHub, защиту ветки, достоверность источника или весь размещённый workflow.
Запросите кандидата и решите, что публиковать
В Actions → Dev Content Pipeline → Run workflow выберите main. Для подготовки кандидата оставьте preview выключенным. Включите его для dry-run с --dry-run --force --max-items 2; он не создаёт коммит кандидата. Уже ожидающий дайджест всё равно приостанавливает генерацию. GitHub описывает ручное управление workflow.
Откройте Draft PR и двуязычный preview. Проверьте дату источника, исходное утверждение и его обоснование, полноту перевода, ссылки и целесообразность публикации. Автоматические проверки кода и схемы не принимают это редакционное решение. При необходимости измените кандидата, подтвердите проверки итогового коммита, затем отметьте готовым и слейте после одобрения публикации. Одна отметка готовности не развёртывает сайт.
Статья о контролях качества объясняет, почему успешный PR может корректно пропустить deploy и что проверять после слияния. О прежней среде Ruby см. запись диагностики Jekyll, сейчас доступную на корейском.
Обсуждение