fix(process): a workflow-permission refusal tells the author to rebase (#730)

Review r1 (Medium): refusalSummary said "повтор и ребейз не помогут" for every
non-stale outcome, and since AC2 the rebase guard's summary carries it too.
For a workflow-permission refusal a rebase and push by the author is exactly
the way out (PROCESS.md §10.4). That outcome now says so; other GitHub
refusals keep the old sentence.

Issue: #730
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
Claude
2026-10-01 07:48:37 +03:00
parent 8e42f4e972
commit 4eb712be0e
2 changed files with 15 additions and 1 deletions
+6 -1
View File
@@ -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 не прислал текста отказа.',
+9
View File
@@ -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' });