mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 15:09:30 +00:00
fix(ci): build push and run links from GITHUB_SERVER_URL (#766)
night-red.mjs fell back to `https://github.com/<repo>/actions/runs/<id>` when the API did not return html_url, and five publishing steps pushed to `https://x-access-token:$TOKEN@github.com/<repo>`. On github.com nothing breaks today; on any other server (GHES) the links and pushes would point to the wrong host while GITHUB_API_URL is already honoured (#751). - ci-proof.mjs: githubServerUrl(env) - GITHUB_SERVER_URL without a trailing slash, github.com when unset; night-red threads it as `server` into commentBody and nightRed, html_url from the API still wins. - _beta-derived, _process (rebase and review document), _ship-review and release-review: `server="${GITHUB_SERVER_URL:-https://github.com}"` and push_url built from it; every token push uses "$push_url". Tests: the night step executed on a non-standard server writes comment and summary links to that host; every token push in every workflow takes its host from GITHUB_SERVER_URL; the four publishing steps executed with GITHUB_SERVER_URL=https://ghe.example.test push to that host, and to github.com when it is empty. Checked: on the previous workflows and with the hard-coded fallback restored in night-red these tests are red. Issue: #766 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:
@@ -28,7 +28,10 @@ test('#697 бот: только по кнопке, прав на запись у
|
||||
assert.doesNotMatch(text, /contents: write/);
|
||||
}
|
||||
const commit = step('Коммит в dev');
|
||||
assert.match(commit, /git push -q "https:\/\/x-access-token:\$TOKEN@github\.com\/\$\{\{ github\.repository \}\}" HEAD:dev/);
|
||||
// #766: хост — сервер раннера (GITHUB_SERVER_URL), не зашитый github.com.
|
||||
assert.match(commit, /push_url="\$\{server%%:\/\/\*\}:\/\/x-access-token:\$TOKEN@\$\{server#\*:\/\/\}\/\$\{\{ github\.repository \}\}"/);
|
||||
assert.match(commit, /git push -q "\$push_url" HEAD:dev/);
|
||||
assert.equal((commit.match(/git push/g) || []).length, 1, 'один push');
|
||||
assert.doesNotMatch(commit, /--force/, 'ушедший dev — перезапуск, а не перезапись');
|
||||
});
|
||||
|
||||
|
||||
+42
-3
@@ -7,7 +7,7 @@ import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { buildCiProof } from '../scripts/ci-proof.mjs';
|
||||
import { buildCiProof, githubServerUrl } from '../scripts/ci-proof.mjs';
|
||||
import { jobInstanceNames, validateJobs } from '../scripts/workflow-jobs.mjs';
|
||||
import { findStep, stepCommand } from './helpers/workflow-step.mjs';
|
||||
import {
|
||||
@@ -386,7 +386,7 @@ function fakeGh(root, issues) {
|
||||
}
|
||||
|
||||
/** Шаг ночи в рабочей копии `cwd` с `scripts/` из этого дерева; асинхронно — сервер живёт в этом процессе. */
|
||||
async function runNightStep(t, { cwd, items, issues = {}, fail = false, scripts = SCRIPTS }) {
|
||||
async function runNightStep(t, { cwd, items, issues = {}, fail = false, scripts = SCRIPTS, env = {} }) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'hp-736-step-'));
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const api = await actionsServer(t, items, { fail });
|
||||
@@ -404,7 +404,7 @@ async function runNightStep(t, { cwd, items, issues = {}, fail = false, scripts
|
||||
env: {
|
||||
...ENV, PATH: `${gh.bin}:${process.env.PATH}`, REPO, RED_RUN: '105',
|
||||
ACTIONS_TOKEN: 'actions-token', GH_TOKEN: 'process-token', GITHUB_API_URL: api.base,
|
||||
GITHUB_STEP_SUMMARY: files.summary, FAKE_LOG: files.log, FAKE_ISSUES: gh.data,
|
||||
GITHUB_STEP_SUMMARY: files.summary, FAKE_LOG: files.log, FAKE_ISSUES: gh.data, ...env,
|
||||
},
|
||||
});
|
||||
let stdout = '';
|
||||
@@ -492,3 +492,42 @@ test('#736 К6 на настоящем bash: сбой API или упавший
|
||||
assert.match(crashed.stdout, /^::warning::красная ночь \(#736\): скрипт упал — комментарии не написаны/m);
|
||||
assert.match(crashed.summary, /- Красная ночь \(#736\): скрипт упал/);
|
||||
});
|
||||
|
||||
// #766: ссылка на прогон без `html_url` строится от сервера раннера
|
||||
// (`GITHUB_SERVER_URL`), а не от зашитого https://github.com.
|
||||
test('#766: сервер ссылок — GITHUB_SERVER_URL без хвостового /, по умолчанию github.com; html_url главнее', () => {
|
||||
assert.equal(githubServerUrl({}), 'https://github.com');
|
||||
assert.equal(githubServerUrl({ GITHUB_SERVER_URL: 'https://ghe.example.test/' }), 'https://ghe.example.test');
|
||||
const R = 'd'.repeat(40);
|
||||
const G = 'a'.repeat(40);
|
||||
const body = commentBody({
|
||||
repo: REPO, red: { id: 5, head_sha: R }, green: { id: 4, head_sha: G, html_url: 'https://api.example/runs/4' },
|
||||
commits: [{ sha: '1'.repeat(40), subject: 'one' }], server: 'https://ghe.example.test',
|
||||
});
|
||||
assert.ok(body.includes(`красный — https://ghe.example.test/${REPO}/actions/runs/5 `), body);
|
||||
assert.ok(body.includes('Последняя зелёная ночь — https://api.example/runs/4 '), 'ссылка API не переписывается');
|
||||
assert.doesNotMatch(body, /github\.com/);
|
||||
});
|
||||
|
||||
test('#766 на настоящем bash: шаг ночи на нестандартном сервере — ссылки прогонов без html_url от GITHUB_SERVER_URL', async (t) => {
|
||||
if (!hasBash()) { t.skip('bash недоступен'); return; }
|
||||
const { cwd, sha } = history(t);
|
||||
const bare = ({ run: { html_url: _dropped, ...run }, context }) => ({ run, context });
|
||||
const items = [
|
||||
validateRun({ id: 105, sha: sha.R, at: day(24), conclusion: 'failure' }),
|
||||
validateRun({ id: 104, sha: sha.c2, at: day(22), full: false }),
|
||||
validateRun({ id: 102, sha: sha.G, at: day(20) }),
|
||||
].map(bare);
|
||||
const run = await runNightStep(t, {
|
||||
cwd, items, env: { GITHUB_SERVER_URL: 'https://ghe.example.test/' },
|
||||
issues: { 1: { state: 'OPEN', comments: [] }, 2: { state: 'OPEN', comments: [] }, 5: { state: 'OPEN', comments: [] }, 8: { state: 'OPEN', comments: [] } },
|
||||
});
|
||||
assert.equal(run.status, 0, run.stderr);
|
||||
assert.doesNotMatch(run.stdout, /::warning::/, run.stdout);
|
||||
const server = `https://ghe.example.test/${REPO}/actions/runs`;
|
||||
assert.ok(run.comment(1).includes(`красный — ${server}/105 `), run.comment(1));
|
||||
assert.ok(run.comment(1).includes(`Последняя зелёная ночь — ${server}/102 `), run.comment(1));
|
||||
assert.ok(run.summary.includes(`- Красный прогон: ${server}/105 `), run.summary);
|
||||
assert.ok(run.summary.includes(`- Отсеян зелёный ${server}/104 `), run.summary);
|
||||
assert.doesNotMatch(run.comment(1) + run.summary, /github\.com/);
|
||||
});
|
||||
|
||||
@@ -2,14 +2,14 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync, spawnSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join, relative, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { PUSH_REFUSAL, classifyPushRefusal, refusalSummary } from '../scripts/merge-candidate.mjs';
|
||||
import { buildIndex } from '../scripts/reviews-index.mjs';
|
||||
import { findStep, runStep } from './helpers/workflow-step.mjs';
|
||||
import { findStep, runStep, workflowSteps } from './helpers/workflow-step.mjs';
|
||||
|
||||
// #723: два шага публикуют коммит и прежде любой отказ push считали сдвигом
|
||||
// ветки — документ ревью релиза в `dev` (release-review.yml, три попытки) и
|
||||
@@ -207,7 +207,7 @@ const noSecrets = (r) => {
|
||||
const TAG = 'v1.78.0';
|
||||
const RELEASE_DOC = `docs/reviews/RELEASE-REVIEW-${TAG}.md`;
|
||||
|
||||
function runRelease(box) {
|
||||
function runRelease(box, extra = {}) {
|
||||
const dir = join(box.temp, 'release-review-result');
|
||||
mkdirSync(dir);
|
||||
const files = {
|
||||
@@ -219,7 +219,7 @@ function runRelease(box) {
|
||||
.map(([name, text]) => `${createHash('sha256').update(text).digest('hex')} ${name}\n`).join(''));
|
||||
return box.run(RELEASE_STEP(), {
|
||||
TAG, DOC: RELEASE_DOC, CANDIDATE: 'c'.repeat(40), BASE: 'v1.77.0', ISSUES: '701,702',
|
||||
RUN_URL: 'https://github.com/o/r/actions/runs/42',
|
||||
RUN_URL: 'https://github.com/o/r/actions/runs/42', ...extra,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -631,11 +631,11 @@ function derivedSandbox(t) {
|
||||
}
|
||||
|
||||
/** Шаг съёмки изменил отпечаток; коммит и push — шагом как есть. */
|
||||
function runDerived(box) {
|
||||
function runDerived(box, extra = {}) {
|
||||
writeFileSync(join(box.work, 'docs', 'images', 'screenshots.json'), '{"fingerprint":"new"}\n');
|
||||
return box.run(DERIVED_STEP(), {
|
||||
TAG: BETA, DOCS_CHANGED: 'true', DOCS_EXPECT: '', GOLDEN_CHANGED: '', GOLDEN_URL: '',
|
||||
GOLDEN_EXPECT_CHANGE: '', GOLDEN_EXPECT_NEW: '', RUN_URL: 'https://github.com/o/r/actions/runs/44', HP_PREPUSH_GATE: '0',
|
||||
GOLDEN_EXPECT_CHANGE: '', GOLDEN_EXPECT_NEW: '', RUN_URL: 'https://github.com/o/r/actions/runs/44', HP_PREPUSH_GATE: '0', ...extra,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -732,6 +732,52 @@ test('#730 AC3: тела _ship-review.yml и _beta-derived.yml — без heredo
|
||||
assert.match(DERIVED_STEP().script, /в dev — проверить перед кандидатом беты\." >> "\$GITHUB_STEP_SUMMARY"\n*$/);
|
||||
});
|
||||
|
||||
// ---------- #766: хост push — сервер раннера, не зашитый github.com ----------
|
||||
|
||||
const SERVER_LINE = 'server="${GITHUB_SERVER_URL:-https://github.com}"';
|
||||
const PUSH_URL_LINE = 'push_url="${server%%://*}://x-access-token:$TOKEN@${server#*://}/${{ github.repository }}"';
|
||||
|
||||
test('#766: каждый push с токеном в workflow — на хост GITHUB_SERVER_URL с запасным github.com', () => {
|
||||
const sites = [];
|
||||
for (const name of readdirSync(WORKFLOWS).filter((file) => /\.ya?ml$/.test(file)).sort()) {
|
||||
for (const step of workflowSteps(readFileSync(join(WORKFLOWS, name), 'utf8'), name)) {
|
||||
if (!step.run || !step.run.includes('x-access-token')) continue;
|
||||
const where = `${name}:${step.line}`;
|
||||
sites.push(where);
|
||||
assert.doesNotMatch(step.run, /@github\.com\b/, `${where}: хост push зашит`);
|
||||
const lines = step.run.split('\n').map((line) => line.trim());
|
||||
assert.ok(lines.includes(SERVER_LINE) && lines.includes(PUSH_URL_LINE), `${where}: адрес push — из GITHUB_SERVER_URL`);
|
||||
assert.ok(lines.indexOf(SERVER_LINE) < lines.indexOf(PUSH_URL_LINE));
|
||||
for (const push of step.run.match(/git push\b(?:[^\n]*\\\n)*[^\n]*/g) ?? []) {
|
||||
assert.match(push, /"\$push_url"/, `${where}: ${push}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepEqual(sites.map((site) => site.split(':')[0]),
|
||||
['_beta-derived.yml', '_process.yml', '_process.yml', '_ship-review.yml', 'release-review.yml'], sites.join(', '));
|
||||
});
|
||||
|
||||
test('#766 на настоящем bash: шаги публикации пушат на хост GITHUB_SERVER_URL', (t) => {
|
||||
if (!hasTools()) { t.skip('bash/tar/jq/sha256sum недоступны'); return; }
|
||||
const server = { GITHUB_SERVER_URL: 'https://ghe.example.test' };
|
||||
// Транспорт подменён: адрес, с которым шаг позвал git push, — в журнале вызовов.
|
||||
const hosts = (r) => r.calls.filter((call) => call.startsWith('push '))
|
||||
.map((call) => /https:\/\/x-access-token:[^@\s]+@(\S+)/.exec(call)?.[1] ?? call);
|
||||
const release = runRelease(sandbox(tempRoot(t, 'hp-766-release-')), server);
|
||||
const docBox = sandbox(tempRoot(t, 'hp-766-doc-'));
|
||||
taskBranch(docBox);
|
||||
const doc = runReviewDoc(docBox, undefined, server);
|
||||
const ship = runShip(sandbox(tempRoot(t, 'hp-766-ship-')), server);
|
||||
const derived = runDerived(derivedSandbox(t), server);
|
||||
for (const [label, r] of [['release-review.yml', release], ['_process.yml', doc], ['_ship-review.yml', ship], ['_beta-derived.yml', derived]]) {
|
||||
assert.equal(r.status, 0, `${label}: ${r.stderr}${r.stdout}`);
|
||||
assert.deepEqual(hosts(r), ['ghe.example.test/o/r'], label);
|
||||
}
|
||||
// Без GITHUB_SERVER_URL — прежний github.com.
|
||||
const plain = runShip(sandbox(tempRoot(t, 'hp-766-plain-')), { GITHUB_SERVER_URL: '' });
|
||||
assert.deepEqual(hosts(plain), ['github.com/o/r']);
|
||||
});
|
||||
|
||||
test('#730: подписи сводки для публикации ship, производных артефактов и стража ребейза', () => {
|
||||
const refusal = classifyPushRefusal(remoteRejected('dev', 'protected branch hook declined'));
|
||||
assert.match(refusalSummary(refusal, { ref: 'dev', stage: 'ship-review' }), /\n\nДокумент пакетного ревью ship не опубликован в `dev`\./);
|
||||
|
||||
Reference in New Issue
Block a user