mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 12:18:51 +00:00
process: preflight does not fail a task branch for foreign causes (#700)
11 of 85 returns in #600–#691 were the thin-workflow mirror check, and any push could turn red because a foreign site behind a docs link was down. - validate.yml preflight: on refs/heads/issue/* the workflow_sync mismatch is a warning in the summary, not a failed verdict; push to dev, the beta candidate and the release keep it red. - On push to dev a mismatch opens one owner issue titled [workflow-sync] (or comments on the open one), like the nightly mutation gate (#472); preflight gets issues: write for that. - check-docs --external=warn: external link failures become warnings; the docs step passes it on task branches only. Canon: PROCESS.md §10.4 («Workflow из ветки по умолчанию»). Issue: #700 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:
@@ -53,12 +53,13 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
# `actions: read` — чтение списка прогонов Validate (#388), `issues: read` —
|
||||
# проверка 8 процессного гейта. Права перечислены явно, потому что job
|
||||
# обращается к API сверх содержимого репозитория.
|
||||
# проверка 8 процессного гейта, `issues: write` — одно issue о расхождении
|
||||
# зеркала workflow на push в dev (#700). Права перечислены явно, потому что
|
||||
# job обращается к API сверх содержимого репозитория.
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
issues: read
|
||||
issues: write
|
||||
steps:
|
||||
# `blob:none` при полной истории (#345): этой job нужны сообщения
|
||||
# коммитов, трейлеры и ИМЕНА изменённых файлов, а не содержимое старых
|
||||
@@ -81,19 +82,24 @@ jobs:
|
||||
id: docs
|
||||
continue-on-error: true
|
||||
env:
|
||||
REF: ${{ github.ref }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
HEAD_MESSAGE: ${{ github.event.head_commit.message }}
|
||||
FULL_INPUT: ${{ inputs.full }}
|
||||
# #697: на ветке задачи `Release:` строгий режим не включает.
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
# #700: на ветке задачи упавший чужой сайт — предупреждение, а не
|
||||
# красный preflight; push в dev, кандидат и релиз судят ссылки строго.
|
||||
external=--external
|
||||
case "$REF" in refs/heads/issue/*) external=--external=warn ;; esac
|
||||
# #586: CLI отдаёт ОДИН ответ. Прежде здесь сравнивался со строкой
|
||||
# `heavy=true` весь вывод `--heavy`, а он двухстрочный: в `$(…)`
|
||||
# строки схлопываются через пробел, сравнение не совпадало никогда,
|
||||
# и строгий режим не включился ни на одном кандидате.
|
||||
mode=$(node scripts/classify-changes.mjs --screenshots-mode)
|
||||
echo "скриншоты документации: режим $mode"
|
||||
node scripts/check-docs.mjs --external --screenshots=$mode
|
||||
node scripts/check-docs.mjs "$external" --screenshots=$mode
|
||||
|
||||
# #635: `docs/reviews/INDEX.md` — снимок каталога ревью; расхождение с
|
||||
# каталогом — невидимые через индекс документы (r2 #635 H1). Гейт стоит
|
||||
@@ -140,6 +146,34 @@ jobs:
|
||||
done
|
||||
exit $status
|
||||
|
||||
# #700: расхождение зеркала на dev — одно открытое issue владельцу, как у
|
||||
# ночного мутационного гейта (#472): чинит его тот, кто зеркалит в main, а
|
||||
# не автор задачи, чья ветка ни при чём. Сбой API — предупреждение: шаг
|
||||
# сообщает, а не судит.
|
||||
- name: "Расхождение зеркала на dev — issue владельцу"
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/dev' && steps.workflow_sync.outcome == 'failure'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
marker="[workflow-sync]"
|
||||
existing=$(gh issue list --repo "$REPO" --state open --search "\"$marker\" in:title" \
|
||||
--json number,title --jq '[.[] | select(.title | startswith("[workflow-sync]"))][0].number // empty' || true)
|
||||
if [ -n "$existing" ]; then
|
||||
gh issue comment "$existing" --repo "$REPO" --body "Расхождение повторилось: $RUN_URL" \
|
||||
|| echo "::warning::комментарий в #$existing не оставлен"
|
||||
exit 0
|
||||
fi
|
||||
cat > /tmp/workflow-sync.md <<EOF
|
||||
Тонкие вызывающие workflow в \`main\` и \`dev\` различаются. Прогон: $RUN_URL
|
||||
|
||||
Тонкий файл исполняется из ветки по умолчанию, поэтому правку триггеров, входов или прав нужно зеркалить в \`main\` (PROCESS.md §10.4, #623). На ветках задач это предупреждение (#700), на push в \`dev\` — красный preflight, пока зеркало не выровнено.
|
||||
EOF
|
||||
gh issue create --repo "$REPO" --title "$marker тонкие workflow в main и dev различаются" \
|
||||
--label infra --label process --body-file /tmp/workflow-sync.md \
|
||||
|| echo "::warning::issue о расхождении зеркала не заведено"
|
||||
|
||||
# Оба гейта ниже судят САМ диапазон коммитов, а не объём проверок, и до
|
||||
# #388 брали его от головы предыдущего пуша. Прогон предыдущего пуша
|
||||
# штатно отменяется следующим (concurrency), и тогда его коммиты не судит
|
||||
@@ -220,6 +254,7 @@ jobs:
|
||||
PROCESS_GATE: ${{ steps.process_gate.outcome }}
|
||||
ACTION_PINS: ${{ steps.action_pins.outcome }}
|
||||
REVIEWS_INDEX: ${{ steps.reviews_index.outcome }}
|
||||
REF: ${{ github.ref }}
|
||||
run: |
|
||||
fail=0
|
||||
check() {
|
||||
@@ -231,9 +266,25 @@ jobs:
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
# #700: причина, к которой ветка задачи отношения не имеет, — в сводку
|
||||
# предупреждением; на dev, кандидате и релизе она красит как прежде.
|
||||
advise() {
|
||||
if [ "$2" = "success" ]; then
|
||||
echo "ok $1"
|
||||
else
|
||||
echo "::warning::$1 ($2) — на ветке задачи предупреждение (#700)"
|
||||
echo "- $1: $2 — предупреждение, чинит релиз-менеджер (#700)" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
}
|
||||
task_branch=false
|
||||
case "$REF" in refs/heads/issue/*) task_branch=true ;; esac
|
||||
echo "### Предполётные проверки" >> "$GITHUB_STEP_SUMMARY"
|
||||
check "документация" "$DOCS"
|
||||
check "тонкие вызывающие workflow в main и dev" "$WORKFLOW_SYNC"
|
||||
if [ "$task_branch" = "true" ]; then
|
||||
advise "тонкие вызывающие workflow в main и dev" "$WORKFLOW_SYNC"
|
||||
else
|
||||
check "тонкие вызывающие workflow в main и dev" "$WORKFLOW_SYNC"
|
||||
fi
|
||||
check "провенанс коммитов" "$PROVENANCE"
|
||||
check "процессный гейт" "$PROCESS_GATE"
|
||||
check "пины сторонних Actions" "$ACTION_PINS"
|
||||
|
||||
+6
-1
@@ -1125,7 +1125,12 @@ Matysh/houseplan-card/.github/workflows/_<имя>.yml@dev` с `secrets: inherit`
|
||||
триггеры, входы ручного запуска или потолок прав; тогда он зеркалится в
|
||||
`main`, и preflight `workflow_sync` в `validate.yml` держит копии равными —
|
||||
сверяются ровно эти шесть файлов, список держит
|
||||
`test/default-branch-workflows.test.mjs`. `performance.yml` в список не входит:
|
||||
`test/default-branch-workflows.test.mjs`. Расхождение красит push в `dev` и
|
||||
заводит одно issue владельцу (`[workflow-sync]`), а на ветке задачи —
|
||||
предупреждение в сводке (#700): к её изменению оно отношения не имеет, и чинит
|
||||
его тот, кто зеркалит в `main`. Так же судятся внешние ссылки документации
|
||||
(`check-docs --external=warn` на ветках `issue/*`): упавший чужой сайт не
|
||||
возвращает задачу. `performance.yml` в список не входит:
|
||||
по расписанию он судит `main` собственным телом из `main`.
|
||||
|
||||
**Цена захода зависит от трека** (#696, решение владельца 2026-09-28). Трек
|
||||
|
||||
@@ -9,7 +9,12 @@ import { freshnessSink, screenshotsMode } from './docs-freshness.mjs';
|
||||
import { guideParityErrors } from './user-guide-parity.mjs';
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const EXTERNAL = process.argv.includes('--external');
|
||||
// `--external` — внешние ссылки судятся ошибкой; `--external=warn` — только
|
||||
// предупреждением (#700): упавший чужой сайт не красит ветку задачи, к чьему
|
||||
// изменению он отношения не имеет. Блокируют внешние ссылки push в dev,
|
||||
// кандидат беты и релиз — там их чинит релиз-менеджер.
|
||||
const EXTERNAL_WARN = process.argv.includes('--external=warn');
|
||||
const EXTERNAL = EXTERNAL_WARN || process.argv.includes('--external');
|
||||
const PUBLIC_DOCS = [
|
||||
'README.md', 'README.ru.md', 'docs/USER-GUIDE.md', 'docs/USER-GUIDE.ru.md',
|
||||
'docs/TOUCH-SUPPORT.md', 'docs/DECOR-EDITOR.md', 'docs/VACUUM.md',
|
||||
@@ -233,6 +238,7 @@ if (!existsSync(manifestPath)) {
|
||||
}
|
||||
|
||||
if (EXTERNAL) {
|
||||
const externalErrors = EXTERNAL_WARN ? warnings : errors;
|
||||
const allowlist = JSON.parse(canonicalText(resolve(ROOT, 'docs/external-link-allowlist.json')));
|
||||
const transientHosts = new Set(allowlist.transientHosts || []);
|
||||
for (const href of [...externalUrls].sort()) {
|
||||
@@ -249,10 +255,10 @@ if (EXTERNAL) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
errors.push(`external link returned ${response.status}: ${href}`);
|
||||
externalErrors.push(`external link returned ${response.status}: ${href}`);
|
||||
} catch (error) {
|
||||
if (transientHosts.has(url.hostname)) warnings.push(`transient external failure: ${href} (${error.message})`);
|
||||
else errors.push(`external link failed: ${href} (${error.message})`);
|
||||
else externalErrors.push(`external link failed: ${href} (${error.message})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12706,6 +12706,29 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: " if (d.startsWith('M') || d.length >= 0) return d;",
|
||||
}],
|
||||
},
|
||||
// #700: предполёт не красит ветку задачи чужими причинами.
|
||||
{
|
||||
id: 'task-branch-workflow-sync-red-again',
|
||||
guard: 'node --test --test-name-pattern="#700" test/validate-workflow.test.mjs',
|
||||
because: '#700: a thin workflow mirror mismatch has nothing to do with the task branch; 11 of 85 '
|
||||
+ 'returns in #600–#691 came from it — on issue/* it is a warning, on dev a red preflight',
|
||||
patches: [{
|
||||
file: '.github/workflows/validate.yml',
|
||||
find: ' advise "тонкие вызывающие workflow в main и dev" "$WORKFLOW_SYNC"',
|
||||
replace: ' check "тонкие вызывающие workflow в main и dev" "$WORKFLOW_SYNC"',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'external-link-warn-mode-ignored',
|
||||
guard: 'node --test --test-name-pattern="#700: check-docs" test/validate-workflow.test.mjs',
|
||||
because: '#700: a foreign site that is down must not turn a task branch red; --external=warn '
|
||||
+ 'routes external failures to warnings',
|
||||
patches: [{
|
||||
file: 'scripts/check-docs.mjs',
|
||||
find: ' const externalErrors = EXTERNAL_WARN ? warnings : errors;',
|
||||
replace: ' const externalErrors = errors; // mutant: warn mode ignored',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'screenshot-freshness-never-strict',
|
||||
guard: 'node --test --test-name-pattern="#586" test/classify-changes.test.mjs',
|
||||
|
||||
@@ -215,7 +215,7 @@ test('#586: preflight спрашивает режим одним значени
|
||||
assert.match(workflow, /classify-changes\.mjs --screenshots-mode/);
|
||||
assert.ok(!/=\s*"heavy=true"/.test(workflow),
|
||||
'сравнение со строкой «heavy=true» вернулось — строгий режим снова не включится');
|
||||
assert.match(workflow, /check-docs\.mjs --external --screenshots=\$mode/);
|
||||
assert.match(workflow, /check-docs\.mjs "\$external" --screenshots=\$mode/);
|
||||
});
|
||||
|
||||
test('#510 AC1 / #601 AC1: мутанты по диффу запрашиваются только кнопкой mutants=true и PR — не пушем, не кандидатом беты, не full', () => {
|
||||
|
||||
@@ -30,8 +30,14 @@ test('check-docs: только две проверки свежести идут
|
||||
assert.ok(viaMode[0].includes('fingerprint is stale'));
|
||||
assert.ok(viaMode[1].includes('capture script changed'));
|
||||
// Хеш картинки, полнота набора сцен и ссылки не имеют права ослабляться.
|
||||
for (const always of ['image hash does not match manifest', 'scenario set is incomplete', 'external link returned']) {
|
||||
for (const always of ['image hash does not match manifest', 'scenario set is incomplete']) {
|
||||
const line = source.split('\n').find((l) => l.includes(always));
|
||||
assert.ok(line && line.includes('errors.push'), `${always} остаётся ошибкой в обоих режимах`);
|
||||
}
|
||||
// #700: внешние ссылки не зависят от режима скриншотов; предупреждением их
|
||||
// делает только явный `--external=warn` на ветке задачи.
|
||||
const external = source.split('\n').find((l) => l.includes('external link returned'));
|
||||
assert.ok(external && external.includes('externalErrors.push'), 'внешняя ссылка идёт в свой сток');
|
||||
assert.match(source, /const externalErrors = EXTERNAL_WARN \? warnings : errors;/);
|
||||
assert.doesNotMatch(source, /externalErrors = freshness/);
|
||||
});
|
||||
|
||||
@@ -440,7 +440,7 @@ test('смоки, golden и performance_smoke условны по heavy (#479)',
|
||||
assert.match(text, /classify-changes\.mjs --heavy/);
|
||||
assert.match(text, /workflow_dispatch:\n\s+inputs:\n\s+full:/);
|
||||
// preflight: режим скриншотов считает тот же скрипт.
|
||||
assert.match(text, /check-docs\.mjs --external --screenshots=\$mode/);
|
||||
assert.match(text, /check-docs\.mjs "\$external" --screenshots=\$mode/);
|
||||
});
|
||||
|
||||
test('ночной прогон — dispatch Validate на dev с full=true (#479)', () => {
|
||||
@@ -613,3 +613,27 @@ test('#541: Validate всегда публикует proof точной попы
|
||||
assert.equal(reuse.includes('lookup-only: true'), false,
|
||||
'marker contents must be restored and verified, not reduced to a cache-hit bit');
|
||||
});
|
||||
|
||||
test('#700: на ветке задачи зеркало workflow и внешние ссылки — предупреждение, на dev — красный и issue', () => {
|
||||
const workflow = read('validate.yml');
|
||||
const preflight = workflow.slice(workflow.indexOf('\n preflight:\n'), workflow.indexOf('\n changes:\n'));
|
||||
const docs = preflight.slice(preflight.indexOf('id: docs'), preflight.indexOf('id: reviews_index'));
|
||||
assert.match(docs, /case "\$REF" in refs\/heads\/issue\/\*\) external=--external=warn ;; esac/);
|
||||
assert.match(docs, /REF: \$\{\{ github\.ref \}\}/);
|
||||
const verdict = preflight.slice(preflight.indexOf('- name: Вердикт предполётных проверок'));
|
||||
assert.match(verdict, /case "\$REF" in refs\/heads\/issue\/\*\) task_branch=true ;; esac/);
|
||||
assert.match(verdict, /if \[ "\$task_branch" = "true" \]; then\n\s+advise "тонкие вызывающие workflow в main и dev" "\$WORKFLOW_SYNC"\n\s+else\n\s+check "тонкие вызывающие workflow в main и dev"/);
|
||||
assert.doesNotMatch(verdict.slice(verdict.indexOf('advise() {'), verdict.indexOf('task_branch=false')), /fail=1/,
|
||||
'предупреждение не красит вердикт');
|
||||
const issue = preflight.slice(preflight.indexOf('- name: "Расхождение зеркала на dev — issue владельцу"'));
|
||||
assert.match(issue, /if: github\.event_name == 'push' && github\.ref == 'refs\/heads\/dev' && steps\.workflow_sync\.outcome == 'failure'/);
|
||||
assert.match(issue, /gh issue list --repo "\$REPO" --state open --search/, 'одно issue, а не одно на каждый push');
|
||||
assert.match(preflight, /permissions:\n\s+contents: read\n\s+actions: read\n\s+issues: write/);
|
||||
});
|
||||
|
||||
test('#700: check-docs --external=warn сводит внешние отказы в предупреждения', () => {
|
||||
const source = read('../../scripts/check-docs.mjs');
|
||||
assert.match(source, /const EXTERNAL_WARN = process\.argv\.includes\('--external=warn'\);/);
|
||||
assert.match(source, /const externalErrors = EXTERNAL_WARN \? warnings : errors;/);
|
||||
assert.equal((source.match(/externalErrors\.push\(/g) || []).length, 2, 'оба вида внешнего отказа');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user