process: a rereview sets S7-code-review again instead of stripping it (#706)

The label step after integration ran one gh call
`--add-label "$TO" --remove-label "$FROM"`. For the rereview outcome
TO == FROM == S7-code-review, and gh added and removed the same label:
#699 was left without a status and no new round started (run
36491087708).

- scripts/status-label.mjs: the same label is removed and set again
  through relabel from process-reconcile (#555), so the labeled event
  starts the next round and a failed restore fails the step; a
  different label is still one call.
- _process.yml: the step calls the script.
- PROCESS.md: the exact-candidate rule names the relabel.
- test/status-label.test.mjs; mutants rereview-relabel-in-one-call and
  process-label-step-combined-again.

Issue: #706
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-09-29 07:19:05 +03:00
parent e7fca7b955
commit ae0e516af1
5 changed files with 137 additions and 4 deletions
+23
View File
@@ -4069,6 +4069,29 @@ const MUTANT_DEFINITIONS = [
replace: ' return safe;',
}],
},
// #706: исход rereview возвращает задачу в ту же метку; один вызов gh её снимал.
{
id: 'rereview-relabel-in-one-call',
guard: 'node --test --test-name-pattern="#706 rereview" test/status-label.test.mjs',
because: '#706: --add-label X --remove-label X in one gh call leaves the issue without a status; '
+ 'the same label must be removed and set again so the labeled event starts a new round',
patches: [{
file: 'scripts/status-label.mjs',
find: ' if (from === to) {',
replace: ' if (false) { // mutant: one combined call',
}],
},
{
id: 'process-label-step-combined-again',
guard: 'node --test --test-name-pattern="#706 шаг конвейера" test/status-label.test.mjs',
because: '#706: the pipeline step must move the label through status-label.mjs, not through the '
+ 'combined gh call that stripped S7-code-review on rereview',
patches: [{
file: '.github/workflows/_process.yml',
find: ' node scripts/status-label.mjs --repo="${{ github.repository }}" \\\n --issue="$NUM" --from="$FROM" --to="$TO"\n',
replace: ' gh issue edit "$NUM" --repo "${{ github.repository }}" \\\n --add-label "$TO" --remove-label "$FROM"\n',
}],
},
{
id: 'vac-trail-drop-warn-removed',
guard: 'node --test --test-name-pattern="#369" test/vacuum.test.mjs',
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env node
/**
* Перестановка статусной метки после интеграции ревью (#706).
*
* node scripts/status-label.mjs --repo=owner/name --issue=NN --from=<метка> --to=<метка>
*
* Обычный исход — одна правка: `--add-label TO --remove-label FROM`. Исход
* `rereview` (#492) возвращает задачу в ту же метку, из которой она пришла:
* `TO == FROM == S7-code-review`. Одним вызовом `gh` такую метку добавляет и
* тут же снимает — задача оставалась без статуса, и нового захода не было
* (#699, 2026-09-28). Поэтому одинаковая метка снимается и ставится заново
* двумя вызовами, как в автосверке (`relabel`, #555): событие `labeled`
* запускает новый заход ревью, а сбой повторной постановки роняет шаг, и
* конвейер зовёт владельца.
*/
import { spawnSync } from 'node:child_process';
import { isMainModule } from './spawn-portable.mjs';
import { relabel } from './process-reconcile.mjs';
function gh(args, { allowFailure = false } = {}) {
const result = spawnSync('gh', args, { encoding: 'utf8' });
if (!allowFailure && (result.error || result.status !== 0)) {
throw new Error(`gh ${args.join(' ')} → ${(result.stderr || result.error?.message || '').trim()}`);
}
return result;
}
/** @returns {'moved' | 'relabeled'} */
export function moveStatusLabel({ repo, issue, from = '', to, execute = gh }) {
if (!repo || !issue || !to) throw new Error('usage: status-label.mjs --repo=owner/name --issue=NN --from=<метка> --to=<метка>');
if (from === to) {
relabel(repo, { number: issue }, to, execute);
return 'relabeled';
}
const args = ['issue', 'edit', String(issue), '--repo', repo, '--add-label', to];
if (from) args.push('--remove-label', from);
execute(args);
return 'moved';
}
if (isMainModule(import.meta.url)) {
const arg = (name) => process.argv.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3) ?? '';
try {
const how = moveStatusLabel({ repo: arg('repo'), issue: arg('issue'), from: arg('from'), to: arg('to') });
console.log(`${arg('from') || '(без метки)'} -> ${arg('to')}${how === 'relabeled' ? ' (снята и поставлена заново — новый заход, #706)' : ''}`);
} catch (error) {
console.error(`::error::${error.message}`);
process.exit(1);
}
}