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:
@@ -214,7 +214,10 @@ jobs:
|
||||
# перезапуск не лечит: причина и ответ git без токена — в журнале и в
|
||||
# сводке шага.
|
||||
push_err="$RUNNER_TEMP/beta-derived-push.stderr"
|
||||
if ! git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev 2> "$push_err"; then
|
||||
# #766: хост push — сервер раннера (GITHUB_SERVER_URL), не зашитый github.com.
|
||||
server="${GITHUB_SERVER_URL:-https://github.com}"
|
||||
push_url="${server%%://*}://x-access-token:$TOKEN@${server#*://}/${{ github.repository }}"
|
||||
if ! git push -q "$push_url" HEAD:dev 2> "$push_err"; then
|
||||
kind=$(node scripts/merge-candidate.mjs --push-refusal="$push_err" --ref=dev \
|
||||
--stage=beta-derived --summary="$GITHUB_STEP_SUMMARY") || kind=unknown
|
||||
if [ "$kind" = "stale" ]; then
|
||||
|
||||
@@ -584,8 +584,11 @@ jobs:
|
||||
# прогона со своей причиной. #730: причина и ответ git без токена —
|
||||
# ещё и в сводке шага (--summary; устаревший lease её не пишет).
|
||||
push_err="$RUNNER_TEMP/rebase-push.stderr"
|
||||
# #766: хост push — сервер раннера (GITHUB_SERVER_URL), не зашитый github.com.
|
||||
server="${GITHUB_SERVER_URL:-https://github.com}"
|
||||
push_url="${server%%://*}://x-access-token:$TOKEN@${server#*://}/${{ github.repository }}"
|
||||
if ! git push -q --force-with-lease="refs/heads/$BRANCH:$before" \
|
||||
"https://x-access-token:$TOKEN@github.com/${{ github.repository }}" \
|
||||
"$push_url" \
|
||||
"HEAD:refs/heads/$BRANCH" 2> "$push_err"; then
|
||||
refusal="$RUNNER_TEMP/push-refusal.md"
|
||||
kind=$(node "$TOOLS/scripts/merge-candidate.mjs" --push-refusal="$push_err" \
|
||||
@@ -1693,7 +1696,10 @@ jobs:
|
||||
# сразу, причина и ответ git без токена — в журнале и в сводке шага;
|
||||
# метка не меняется.
|
||||
push_err="$RUNNER_TEMP/review-doc-push.stderr"
|
||||
if ! git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" \
|
||||
# #766: хост push — сервер раннера (GITHUB_SERVER_URL), не зашитый github.com.
|
||||
server="${GITHUB_SERVER_URL:-https://github.com}"
|
||||
push_url="${server%%://*}://x-access-token:$TOKEN@${server#*://}/${{ github.repository }}"
|
||||
if ! git push -q "$push_url" \
|
||||
"HEAD:$target" 2> "$push_err"; then
|
||||
kind=$(node "$TOOLS/scripts/merge-candidate.mjs" --push-refusal="$push_err" --ref="$target" \
|
||||
--stage=review-doc --summary="$GITHUB_STEP_SUMMARY") || kind=unknown
|
||||
@@ -1713,7 +1719,7 @@ jobs:
|
||||
# После ребейза набор путей другой — проверяется заново. Форс здесь
|
||||
# запрещён и не появляется: ветка двигается только вперёд.
|
||||
git diff --name-only "origin/$target...HEAD" | node "$TOOLS/scripts/review-doc-guard.mjs"
|
||||
if ! git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" \
|
||||
if ! git push -q "$push_url" \
|
||||
"HEAD:$target" 2> "$push_err"; then
|
||||
kind=$(node "$TOOLS/scripts/merge-candidate.mjs" --push-refusal="$push_err" --ref="$target" \
|
||||
--stage=review-doc --summary="$GITHUB_STEP_SUMMARY") || kind=unknown
|
||||
|
||||
@@ -341,6 +341,9 @@ jobs:
|
||||
fi
|
||||
msg="$RUNNER_TEMP/ship-review-commit.txt"
|
||||
push_err="$RUNNER_TEMP/ship-review-push.stderr"
|
||||
# #766: хост push — сервер раннера (GITHUB_SERVER_URL), не зашитый github.com.
|
||||
server="${GITHUB_SERVER_URL:-https://github.com}"
|
||||
push_url="${server%%://*}://x-access-token:$TOKEN@${server#*://}/${{ github.repository }}"
|
||||
for attempt in 1 2 3; do
|
||||
git fetch -q origin dev
|
||||
git reset -q --hard origin/dev
|
||||
@@ -375,7 +378,7 @@ jobs:
|
||||
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
|
||||
commit -q -F "$msg"
|
||||
git diff --name-only "origin/dev...HEAD" | node scripts/review-doc-guard.mjs
|
||||
if git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev 2> "$push_err"; then
|
||||
if git push -q "$push_url" HEAD:dev 2> "$push_err"; then
|
||||
if [ "$MODE" = "nightly" ]; then
|
||||
echo "### Ночное пакетное ревью ship" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
|
||||
@@ -309,6 +309,9 @@ jobs:
|
||||
# Сообщение коммита — построчно в файл, без heredoc в `run:` (#723).
|
||||
msg="$RUNNER_TEMP/release-review-commit.txt"
|
||||
push_err="$RUNNER_TEMP/release-review-push.stderr"
|
||||
# #766: хост push — сервер раннера (GITHUB_SERVER_URL), не зашитый github.com.
|
||||
server="${GITHUB_SERVER_URL:-https://github.com}"
|
||||
push_url="${server%%://*}://x-access-token:$TOKEN@${server#*://}/${{ github.repository }}"
|
||||
for attempt in 1 2 3; do
|
||||
git fetch -q origin dev
|
||||
git reset -q --hard origin/dev
|
||||
@@ -337,7 +340,7 @@ jobs:
|
||||
-c user.email="209825114+claude[bot]@users.noreply.github.com" \
|
||||
commit -q -F "$msg"
|
||||
git diff --name-only "origin/dev...HEAD" | node scripts/review-doc-guard.mjs
|
||||
if git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev 2> "$push_err"; then
|
||||
if git push -q "$push_url" HEAD:dev 2> "$push_err"; then
|
||||
echo "### Независимое ревью $TAG" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Итог: $counts — \`$DOC\` в dev. Выпуск не блокируется." >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "::notice::$DOC опубликован: $counts"
|
||||
|
||||
@@ -501,6 +501,8 @@ const apiHeaders = (token) => ({
|
||||
* `archive_download_url` приходит из API абсолютным и базу не берёт.
|
||||
*/
|
||||
export const githubApiBase = (env = process.env) => String(env.GITHUB_API_URL || 'https://api.github.com').replace(/\/+$/, '');
|
||||
/** Сервер GitHub для ссылок (#766): `GITHUB_SERVER_URL`, как у раннера, без хвостового `/`. */
|
||||
export const githubServerUrl = (env = process.env) => String(env.GITHUB_SERVER_URL || 'https://github.com').replace(/\/+$/, '');
|
||||
|
||||
async function githubJson(url, token, fetchImpl) {
|
||||
const response = await fetchImpl(url, { headers: apiHeaders(token) });
|
||||
|
||||
+16
-11
@@ -36,7 +36,7 @@ import { isMainModule } from './spawn-portable.mjs';
|
||||
import { classify } from './change-classes.mjs';
|
||||
import { issueTrailers } from './release-membership.mjs';
|
||||
import { hasReleaseTrailer } from './ship-review.mjs';
|
||||
import { CI_PROOF_POLICIES, evaluateCiProof, githubApiBase, loadGithubProofContext } from './ci-proof.mjs';
|
||||
import { CI_PROOF_POLICIES, evaluateCiProof, githubApiBase, githubServerUrl, loadGithubProofContext } from './ci-proof.mjs';
|
||||
|
||||
/** Сколько последних dispatch-прогонов Validate на `dev` смотрит поиск `G`. */
|
||||
export const RUN_WINDOW = 50;
|
||||
@@ -46,7 +46,8 @@ export const NIGHT_RED_MARKER_RE = /<!-- hp:night-red green=([0-9a-f]{40,64}) re
|
||||
|
||||
const short = (sha, n) => String(sha || '').slice(0, n);
|
||||
const stamp = (run) => Date.parse(run?.created_at || '') || 0;
|
||||
const runUrl = (repo, run) => run?.html_url || `https://github.com/${repo}/actions/runs/${run?.id}`;
|
||||
// Ссылка API (`html_url`) главнее; без неё — сервер раннера (#766), не зашитый github.com.
|
||||
const runUrl = (repo, run, server) => run?.html_url || `${server}/${repo}/actions/runs/${run?.id}`;
|
||||
const subjectOf = (message) => String(message || '').split(/\r?\n/)[0].trim();
|
||||
|
||||
/**
|
||||
@@ -130,13 +131,16 @@ const limited = (items, render) => {
|
||||
return shown.join('; ');
|
||||
};
|
||||
|
||||
/** К4: тело комментария. `commits` — все коммиты задачи в диапазоне; метка несёт все. */
|
||||
export function commentBody({ repo, red, green, commits = [], failedJobs = [] }) {
|
||||
/**
|
||||
* К4: тело комментария. `commits` — все коммиты задачи в диапазоне; метка несёт все.
|
||||
* `server` — для ссылок прогонов без `html_url` (#766).
|
||||
*/
|
||||
export function commentBody({ repo, red, green, commits = [], failedJobs = [], server = githubServerUrl() }) {
|
||||
const R = red.head_sha;
|
||||
const G = green.head_sha;
|
||||
return [
|
||||
`**Красная ночь:** полный Validate на \`dev\` красный — ${runUrl(repo, red)} (\`${short(R, 12)}\`).`
|
||||
+ ` Последняя зелёная ночь — ${runUrl(repo, green)} (\`${short(G, 12)}\`).`,
|
||||
`**Красная ночь:** полный Validate на \`dev\` красный — ${runUrl(repo, red, server)} (\`${short(R, 12)}\`).`
|
||||
+ ` Последняя зелёная ночь — ${runUrl(repo, green, server)} (\`${short(G, 12)}\`).`,
|
||||
`Коммиты этой задачи с файлами классов A/B вошли в \`dev\` между ними: ${limited(commits, (c) => `\`${short(c.sha, 8)}\` ${c.subject}`)}.`,
|
||||
`Упали job: ${failedJobs.length ? limited(failedJobs, (name) => name) : '—'}.`,
|
||||
'Задача — подозреваемая по диапазону, а не виновная. Ночь — сигнал, не гейт: бета по-прежнему требует зелёный Validate на SHA кандидата.',
|
||||
@@ -156,10 +160,10 @@ const VERDICT_LINE = {
|
||||
* `rangeCommits`. Возвращает строки сводки, написанные комментарии и
|
||||
* предупреждения; сбой до первой задачи — исключение.
|
||||
*/
|
||||
export async function nightRed({ repo, redRunId, api, issues, git, workflowJobs = undefined }) {
|
||||
export async function nightRed({ repo, redRunId, api, issues, git, workflowJobs = undefined, server = githubServerUrl() }) {
|
||||
const head = '### Красная ночь (#736)';
|
||||
const red = await api.run(redRunId);
|
||||
const redLine = `${runUrl(repo, red)} (\`${short(red.head_sha, 12)}\`)`;
|
||||
const redLine = `${runUrl(repo, red, server)} (\`${short(red.head_sha, 12)}\`)`;
|
||||
if (red.status !== 'completed' || red.conclusion !== 'failure') {
|
||||
return {
|
||||
posted: [], warnings: [],
|
||||
@@ -171,14 +175,14 @@ export async function nightRed({ repo, redRunId, api, issues, git, workflowJobs
|
||||
});
|
||||
const summary = [head, `- Красный прогон: ${redLine}.`];
|
||||
for (const item of found.skipped) {
|
||||
summary.push(`- Отсеян зелёный ${runUrl(repo, item.run)} (\`${short(item.run.head_sha, 12)}\`): ${item.status} — ${item.note}.`);
|
||||
summary.push(`- Отсеян зелёный ${runUrl(repo, item.run, server)} (\`${short(item.run.head_sha, 12)}\`): ${item.status} — ${item.note}.`);
|
||||
}
|
||||
if (!found.run) {
|
||||
summary.push(`- Последняя зелёная ночь не найдена (просмотрено прогонов: ${found.looked}) — подозреваемых не назначаю.`);
|
||||
return { posted: [], warnings: [], summary };
|
||||
}
|
||||
const green = found.run;
|
||||
summary.push(`- Последняя зелёная ночь: ${runUrl(repo, green)} (\`${short(green.head_sha, 12)}\`).`);
|
||||
summary.push(`- Последняя зелёная ночь: ${runUrl(repo, green, server)} (\`${short(green.head_sha, 12)}\`).`);
|
||||
const commits = green.head_sha === red.head_sha ? [] : git.rangeCommits(green.head_sha, red.head_sha);
|
||||
if (!commits.length) {
|
||||
summary.push('- Тот же код был зелёным — вероятен флак. Комментариев нет.');
|
||||
@@ -195,7 +199,7 @@ export async function nightRed({ repo, redRunId, api, issues, git, workflowJobs
|
||||
const issue = issues.view(suspect.number);
|
||||
const verdict = commentVerdict({ issue, green: green.head_sha, commits: suspect.commits });
|
||||
if (verdict === 'comment') {
|
||||
const body = commentBody({ repo, red, green, commits: suspect.commits, failedJobs });
|
||||
const body = commentBody({ repo, red, green, commits: suspect.commits, failedJobs, server });
|
||||
issues.comment(suspect.number, body);
|
||||
posted.push({ number: suspect.number, body });
|
||||
}
|
||||
@@ -289,6 +293,7 @@ if (isMainModule(import.meta.url)) {
|
||||
api: actionsClient({ repo, token: process.env.ACTIONS_TOKEN || '', apiBase: githubApiBase() }),
|
||||
issues: ghIssues({ repo }),
|
||||
git: gitClient(),
|
||||
server: githubServerUrl(),
|
||||
});
|
||||
const text = result.summary.join('\n');
|
||||
console.log(text);
|
||||
|
||||
@@ -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