mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
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:
@@ -1768,10 +1768,13 @@ jobs:
|
||||
# когда кандидат не слит (конфликт, красный Validate, изменившийся
|
||||
# patch-id, ушедший dev). Без исхода от скрипта — как раньше: S6.
|
||||
TO: ${{ (needs.guard.outputs.stage == 'code' && steps.decide.outputs.green == 'true' && steps.merge.outputs.to != '') && steps.merge.outputs.to || ((needs.guard.outputs.stage == 'code' && steps.decide.outputs.green == 'true' && steps.merge.outputs.merged != 'true') && 'S6-in-progress' || steps.decide.outputs.to) }}
|
||||
# #706: исход `rereview` возвращает задачу в ту же метку. Одним вызовом
|
||||
# `gh` она добавлялась и тут же снималась — задача оставалась без
|
||||
# статуса. Скрипт снимает и ставит её заново, чтобы событие `labeled`
|
||||
# запустило новый заход.
|
||||
run: |
|
||||
gh issue edit "$NUM" --repo "${{ github.repository }}" \
|
||||
--add-label "$TO" --remove-label "$FROM"
|
||||
echo "$FROM -> $TO"
|
||||
node scripts/status-label.mjs --repo="${{ github.repository }}" \
|
||||
--issue="$NUM" --from="$FROM" --to="$TO"
|
||||
|
||||
- name: Сводка длительности стадий
|
||||
if: always()
|
||||
|
||||
+2
-1
@@ -1289,7 +1289,8 @@ npm ci, Python и Chromium, оставаясь исполненной job: до
|
||||
- если `dev` не двигался — push с `--force-with-lease` на текущую вершину;
|
||||
- если двигался — ребейз (конфликт — `S6-in-progress`, как раньше), сравнение
|
||||
patch-id проверенного и получившегося диффа (различие — `S7-code-review`: вердикт
|
||||
к другому диффу не применим, §2.10), публикация кандидата в ветку задачи, запуск
|
||||
к другому диффу не применим, §2.10; метка снимается и ставится заново, чтобы
|
||||
событие запустило новый заход, #706), публикация кандидата в ветку задачи, запуск
|
||||
Validate с мутантами на ней (#510) и ожидание зелёного dispatch-прогона **на этом
|
||||
SHA** — push-прогон мутантов не несёт — и только затем push в `dev` с lease на ту
|
||||
вершину, поверх которой кандидат собран. На `show`/`ship` мутантов нет, и
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// #706: исход rereview возвращает задачу в ту же метку `S7-code-review`, и
|
||||
// одним вызовом `gh` она добавлялась и тут же снималась — задача оставалась без
|
||||
// статуса, новый заход не стартовал.
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { moveStatusLabel } from '../scripts/status-label.mjs';
|
||||
|
||||
const REPO = 'Matysh/houseplan-card';
|
||||
const recorder = (fail = () => false) => {
|
||||
const calls = [];
|
||||
const execute = (args, opts = {}) => {
|
||||
calls.push(args.slice(5).join(' '));
|
||||
const status = fail(args, calls.length) ? 1 : 0;
|
||||
if (status && !opts.allowFailure) throw new Error(`gh ${args.join(' ')} → boom`);
|
||||
return { status, stderr: status ? 'boom' : '' };
|
||||
};
|
||||
return { calls, execute };
|
||||
};
|
||||
|
||||
test('#706 rereview: та же метка снимается и ставится заново отдельными вызовами', () => {
|
||||
const { calls, execute } = recorder();
|
||||
const how = moveStatusLabel({ repo: REPO, issue: 699, from: 'S7-code-review', to: 'S7-code-review', execute });
|
||||
assert.equal(how, 'relabeled');
|
||||
assert.deepEqual(calls, ['--remove-label S7-code-review', '--add-label S7-code-review'],
|
||||
'снятие, затем постановка — событие labeled запускает новый заход');
|
||||
});
|
||||
|
||||
test('#706 обычный исход — одна правка: новая метка ставится, прежняя снимается', () => {
|
||||
const { calls, execute } = recorder();
|
||||
assert.equal(moveStatusLabel({ repo: REPO, issue: 700, from: 'S7-code-review', to: 'S8-merged', execute }), 'moved');
|
||||
assert.deepEqual(calls, ['--add-label S8-merged --remove-label S7-code-review']);
|
||||
const none = recorder();
|
||||
moveStatusLabel({ repo: REPO, issue: 700, from: '', to: 'S6-in-progress', execute: none.execute });
|
||||
assert.deepEqual(none.calls, ['--add-label S6-in-progress'], 'без исходной метки снимать нечего');
|
||||
});
|
||||
|
||||
test('#706 сбой повторной постановки роняет шаг, а не оставляет задачу без статуса молча', () => {
|
||||
const once = recorder((args, n) => n === 2);
|
||||
moveStatusLabel({ repo: REPO, issue: 699, from: 'S7-code-review', to: 'S7-code-review', execute: once.execute });
|
||||
assert.deepEqual(once.calls, ['--remove-label S7-code-review', '--add-label S7-code-review', '--add-label S7-code-review'],
|
||||
'одна попытка восстановления, как в автосверке (#555)');
|
||||
const always = recorder((args) => args.includes('--add-label'));
|
||||
assert.throws(() => moveStatusLabel({ repo: REPO, issue: 699, from: 'S7-code-review', to: 'S7-code-review', execute: always.execute }),
|
||||
/could not restore S7-code-review/);
|
||||
assert.throws(() => moveStatusLabel({ repo: REPO, issue: 699, from: 'S7-code-review', to: '', execute: always.execute }), /usage/);
|
||||
});
|
||||
|
||||
test('#706 шаг конвейера переставляет метку через скрипт, а не одним вызовом gh', () => {
|
||||
const workflow = readFileSync(fileURLToPath(new URL('../.github/workflows/_process.yml', import.meta.url)), 'utf8');
|
||||
const step = workflow.slice(workflow.indexOf('- name: Переставить метку'), workflow.indexOf('- name: Сводка длительности стадий'));
|
||||
assert.ok(step.length > 0, 'шаг найден');
|
||||
assert.match(step, /node scripts\/status-label\.mjs --repo="\$\{\{ github\.repository \}\}" \\\n\s+--issue="\$NUM" --from="\$FROM" --to="\$TO"/);
|
||||
assert.doesNotMatch(step, /gh issue edit/, 'совмещённый вызов снимал ту же метку, которую ставил');
|
||||
});
|
||||
Reference in New Issue
Block a user