diff --git a/PROCESS.md b/PROCESS.md index 2b3d1531..a3f9d8a1 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -701,9 +701,9 @@ demo/docs/capture.mjs`, коммит вместе с задачей. достаточен для продолжения релиза, повторное код-ревью не требуется — §11.4. **Гейт стабильного релиза:** полный локальный прогон плюс Validate и Full -Performance зелёные на точном SHA, плюс зелёный E2E на реальном Home Assistant -на теге (`houseplan-e2e`, запускает и ждёт `release.yml`, #514); статусов -issue не касается. +Performance зелёные на точном SHA, плюс зелёный E2E на реальном Home Assistant: +`release.yml` сам запускает `e2e.yml` в `houseplan-e2e` на теге и ждёт его +зелёного (#514); статусов issue не касается. --- diff --git a/scripts/e2e-gate.mjs b/scripts/e2e-gate.mjs index 9abec3f1..68a2cdfd 100755 --- a/scripts/e2e-gate.mjs +++ b/scripts/e2e-gate.mjs @@ -25,7 +25,7 @@ export const E2E_REPO = 'Matysh/houseplan-e2e'; export const E2E_WORKFLOW = 'e2e.yml'; /** Допуск на расхождение часов раннера и GitHub при отборе «свежих» прогонов. */ export const CLOCK_SKEW_MS = 60_000; -export const TOKEN_HINT = 'нужен секрет E2E_DISPATCH_TOKEN с правом Actions: write на houseplan-e2e'; +export const TOKEN_HINT = 'нужен секрет E2E_DISPATCH_TOKEN: Actions: write на houseplan-e2e И чтение релизов houseplan-card (fine-grained PAT — оба репозитория в списке)'; export const CARD_REPO = 'Matysh/houseplan-card'; /** @@ -74,6 +74,9 @@ export function classifyRun(jobs, tag) { export async function e2eGate({ tag, ops, appearMs = VALIDATE_APPEAR_MS, totalMs = VALIDATE_TOTAL_MS, pollMs = POLL_MS }) { const started = ops.now(); try { + // Список релизов читается ДО dispatch и обязан падать громко (ревью r3 M1): + // fine-grained токен «только houseplan-e2e» не видит houseplan-card, и + // тихий пустой список дал бы upgrade_from=stable — тег сам на себя. await ops.dispatch(tag, previousStable(await ops.releases(), tag)); } catch (error) { const message = String(error?.message || error); @@ -116,7 +119,11 @@ export function realOps({ repo = E2E_REPO, workflow = E2E_WORKFLOW, cardRepo = C const fields = 'databaseId,status,conclusion,url,createdAt'; const parse = (r) => (r.status === 0 && r.stdout ? JSON.parse(r.stdout) : []); return { - releases: async () => parse(exec('gh', ['release', 'list', '--repo', cardRepo, '--json', 'tagName,isDraft,isPrerelease', '--limit', '30'])), + releases: async () => { + const r = exec('gh', ['release', 'list', '--repo', cardRepo, '--json', 'tagName,isDraft,isPrerelease', '--limit', '30']); + if (r.status !== 0) throw new Error(`gh release list ${cardRepo}: ${(r.stderr || r.stdout || '').trim()}`); + return r.stdout ? JSON.parse(r.stdout) : []; + }, dispatch: async (tag, upgradeFrom = 'stable') => { const r = exec('gh', ['workflow', 'run', workflow, '--repo', repo, '--ref', 'main', '-f', `houseplan_ref=${tag}`, '-f', `upgrade_from=${upgradeFrom}`, '-f', 'ha_version=stable']); diff --git a/test/e2e-gate.test.mjs b/test/e2e-gate.test.mjs index e2145961..d6570890 100755 --- a/test/e2e-gate.test.mjs +++ b/test/e2e-gate.test.mjs @@ -114,6 +114,18 @@ test('#514 AC1: a dispatch refused by the token is an error that names the missi assert.equal(fake.calls(), 0, 'no polling after a failed dispatch'); }); +test('#514 r3 M1: a token that cannot read houseplan-card releases fails loudly with the token hint, never dispatches with upgrade_from=stable', async () => { + const fake = fakeOps({ snapshots: [[]] }); + fake.ops.releases = async () => { throw new Error('gh release list Matysh/houseplan-card: HTTP 404: Not Found'); }; + const outcome = await e2eGate({ tag: TAG, ops: fake.ops, pollMs: 1000 }); + assert.equal(outcome.result, 'error'); + assert.deepEqual(fake.dispatched, [], 'no dispatch on a broken release list'); + assert.ok(outcome.note.includes('gh release list'), outcome.note); + // realOps: a failing gh is an exception, not an empty list + const exec = () => ({ status: 1, stdout: '', stderr: 'HTTP 403: Resource not accessible by personal access token' }); + await assert.rejects(() => realOps({ exec }).releases(), /403/); +}); + test('#514: the upgrade suite starts from the previous stable, never from the tag under test', () => { const releases = [ { tagName: 'v1.74.0', isDraft: false, isPrerelease: false },