10d2c48cf0
SHA-256 считался локально от уже скачанного файла. Это защищает target от последующей подмены, но не доказывает, что builder скачал именно ожидаемый upstream artifact: сумма фиксирует то, что пришло, каким бы оно ни было. То есть trust-on-first-use, а не проверка происхождения. Upstream публикует контрольные суммы релиза ассетом hashes.txt: 6493dfff...f94 build/hysteria-linux-amd64 f24f63be...189 build/hysteria-linux-amd64-avx Теперь резолвер отдаёт и URL этого ассета, сборка скачивает его, берёт оттуда ожидаемую сумму и сверяет с ней бинарник - и только после этого записывает SHA-256 в HY2XS lock и metadata. Сопоставление идёт по базовому имени и строго на равенство: build/ - часть пути, а hysteria-linux-amd64-avx - другой артефакт, который не должен совпасть по префиксу. Разбор вынесен в parseUpstreamHashes и покрыт тестами, включая форму sha256:<hex>, верхний регистр, противоречивые и отсутствующие записи. Релиз без hashes.txt для production-сборки непригоден и отклоняется. Источник ожидаемой суммы фиксируется в metadata как hysteria_sha_source.
126 lines
3.6 KiB
TypeScript
126 lines
3.6 KiB
TypeScript
/**
|
|
* Build-time CLI: определяет upstream-релиз Hysteria и печатает env-строки.
|
|
*
|
|
* Запускается только на build machine из tools/build/lib/hysteria.sh.
|
|
* Target-сервер этот код никогда не выполняет.
|
|
*
|
|
* bun run tools/resolve-hysteria.ts --channel stable
|
|
* bun run tools/resolve-hysteria.ts --channel stable --version v2.12.2
|
|
*/
|
|
|
|
import {
|
|
HYSTERIA_UPSTREAM_REPO,
|
|
selectLatestStableRelease,
|
|
selectReleaseByVersion,
|
|
type GithubRelease,
|
|
type ResolvedHysteriaRelease
|
|
} from "../src/build/hysteriaRelease";
|
|
|
|
const RELEASES_PER_PAGE = 100;
|
|
const MAX_PAGES = 5;
|
|
|
|
function fail(message: string): never {
|
|
console.error(`[hy2xs-build] ERROR: ${message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
function parseArgs(argv: string[]): { channel: string; version: string } {
|
|
let channel = "stable";
|
|
let version = "";
|
|
|
|
for (let i = 0; i < argv.length; i += 1) {
|
|
const arg = argv[i];
|
|
if (arg === "--channel") {
|
|
channel = argv[i + 1] ?? "";
|
|
i += 1;
|
|
continue;
|
|
}
|
|
if (arg === "--version") {
|
|
version = argv[i + 1] ?? "";
|
|
i += 1;
|
|
continue;
|
|
}
|
|
fail(`unknown argument: ${arg}`);
|
|
}
|
|
|
|
if (channel !== "stable") {
|
|
fail(`unsupported HYSTERIA_CHANNEL for resolution: ${channel} (expected stable)`);
|
|
}
|
|
|
|
return { channel, version: version.trim() };
|
|
}
|
|
|
|
async function fetchReleases(): Promise<GithubRelease[]> {
|
|
const headers: Record<string, string> = {
|
|
Accept: "application/vnd.github+json",
|
|
"X-GitHub-Api-Version": "2022-11-28",
|
|
"User-Agent": "hy2xs-build"
|
|
};
|
|
|
|
const token = (process.env.GITHUB_TOKEN ?? "").trim();
|
|
if (token) {
|
|
headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
|
|
const releases: GithubRelease[] = [];
|
|
|
|
for (let page = 1; page <= MAX_PAGES; page += 1) {
|
|
const url = `https://api.github.com/repos/${HYSTERIA_UPSTREAM_REPO}/releases?per_page=${RELEASES_PER_PAGE}&page=${page}`;
|
|
const response = await fetch(url, { headers });
|
|
|
|
if (response.status === 403 || response.status === 429) {
|
|
fail(
|
|
`GitHub API rate limit reached (HTTP ${response.status}). ` +
|
|
"Set GITHUB_TOKEN, or build with HYSTERIA_CHANNEL=pinned."
|
|
);
|
|
}
|
|
if (!response.ok) {
|
|
fail(`GitHub API request failed: HTTP ${response.status} for ${url}`);
|
|
}
|
|
|
|
const payload = (await response.json()) as unknown;
|
|
if (!Array.isArray(payload)) {
|
|
fail(`unexpected GitHub API payload for ${url}`);
|
|
}
|
|
|
|
releases.push(...(payload as GithubRelease[]));
|
|
|
|
if (payload.length < RELEASES_PER_PAGE) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (releases.length === 0) {
|
|
fail(`no releases returned for ${HYSTERIA_UPSTREAM_REPO}`);
|
|
}
|
|
|
|
return releases;
|
|
}
|
|
|
|
function emit(resolved: ResolvedHysteriaRelease, resolution: string): void {
|
|
// Значения читаются bash через eval-free парсинг, поэтому печатаем по строке.
|
|
console.log(`HYSTERIA_VERSION=${resolved.version}`);
|
|
console.log(`HYSTERIA_TAG=${resolved.tag}`);
|
|
console.log(`HYSTERIA_ARTIFACT_URL=${resolved.artifactUrl}`);
|
|
console.log(`HYSTERIA_HASHES_URL=${resolved.hashesUrl}`);
|
|
console.log(`HYSTERIA_PUBLISHED_AT=${resolved.publishedAt}`);
|
|
console.log(`HYSTERIA_RESOLUTION=${resolution}`);
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const { version } = parseArgs(Bun.argv.slice(2));
|
|
const releases = await fetchReleases();
|
|
|
|
try {
|
|
if (version) {
|
|
emit(selectReleaseByVersion(releases, version), "override");
|
|
return;
|
|
}
|
|
emit(selectLatestStableRelease(releases), "latest-stable");
|
|
} catch (error) {
|
|
fail(error instanceof Error ? error.message : String(error));
|
|
}
|
|
}
|
|
|
|
await main();
|