diff --git a/scripts/merge-candidate.mjs b/scripts/merge-candidate.mjs index 7eaeefbb..3c623e20 100755 --- a/scripts/merge-candidate.mjs +++ b/scripts/merge-candidate.mjs @@ -538,6 +538,11 @@ export function refusalSummary(refusal, { ref = '', stage = '' } = {}) { : refusal.kind === PUSH_REFUSAL.remote ? 'GitHub отклонил push сам (правило ветки, хук, сбой сервера)' : 'git push не удался, и это не отказ по lease (сеть, аутентификация)'; + // #730 r1: при отказе по праву на workflow ребейз — штатный выход, только + // делает его автор (PROCESS.md §10.4). «Ребейз не поможет» говорило обратное. + const nextStep = refusal.kind === PUSH_REFUSAL.workflow + ? `Это не сдвиг \`${ref}\`: повтор этого шага не поможет, шаг остановлен без повторов. Кандидат меняет workflow-файл, токен конвейера не может его опубликовать: ребейз и push делает автор, либо владелец выдаёт право.` + : `Это не сдвиг \`${ref}\`: повтор и ребейз не помогут, шаг остановлен без повторов.`; // Ответ GitHub не должен открыть или закрыть блок кода сводки. const unfence = (text) => String(text || '').replace(/```/g, "'''"); const files = (refusal.files || []).map((file) => `\`${file}\``).join(', '); @@ -545,7 +550,7 @@ export function refusalSummary(refusal, { ref = '', stage = '' } = {}) { return [ `### git push в \`${ref}\` отклонён: ${refusal.kind} (#723)`, '', - `${what} не опубликован в \`${ref}\`. ${why}. Это не сдвиг \`${ref}\`: повтор и ребейз не помогут, шаг остановлен без повторов.`, + `${what} не опубликован в \`${ref}\`. ${why}. ${nextStep}`, ...(refusal.reason ? ['', `Причина, которую назвал GitHub: «${unfence(refusal.reason)}».${files ? ` Файлы: ${files}.` : ''}`] : []), '', stderr ? `Ответ git:\n\n\`\`\`\n${stderr}\n\`\`\`` : 'git не прислал текста отказа.', diff --git a/test/publish-push-refusal.test.mjs b/test/publish-push-refusal.test.mjs index 2474b4c2..1cde6987 100644 --- a/test/publish-push-refusal.test.mjs +++ b/test/publish-push-refusal.test.mjs @@ -579,6 +579,15 @@ test('#730: подписи сводки для публикации ship, про assert.match(refusalSummary(refusal, { ref: 'issue/9-fix', stage: 'rebase' }), /\n\nРебейз ветки на dev не опубликован в `issue\/9-fix`\./); }); +test('#730 r1: при отказе по праву на workflow сводка зовёт автора сделать ребейз, а не отговаривает', () => { + const workflow = refusalSummary(classifyPushRefusal(remoteRejected('issue/9-fix', WORKFLOW_REASON)), { ref: 'issue/9-fix', stage: 'rebase' }); + assert.match(workflow, /ребейз и push делает автор, либо владелец выдаёт право/); + assert.doesNotMatch(workflow, /ребейз не помогут/); + const remote = refusalSummary(classifyPushRefusal(remoteRejected('dev', 'protected branch hook declined')), { ref: 'dev', stage: 'ship-review' }); + assert.match(remote, /повтор и ребейз не помогут/); + assert.doesNotMatch(remote, /ребейз и push делает автор/); +}); + test('#723 AC2: сводка об отказе — без токена, URL с учётными данными и Authorization; причина и файлы названы', () => { const refusal = classifyPushRefusal(`> Authorization: Bearer ${OTHER_TOKEN}\n${remoteRejected('dev', WORKFLOW_REASON)}`, { secrets: [FAKE_TOKEN] }); const text = refusalSummary(refusal, { ref: 'dev', stage: 'release-review' });