Android / pre-check (push) Failing after 8s
Android / android (push) Skipped
CIFuzz / pre-check (push) Failing after 3s
CIFuzz / fuzzing (address) (push) Skipped
CIFuzz / fuzzing (undefined) (push) Skipped
CodeQL / pre-check (push) Failing after 4s
CodeQL / analyze (python) (push) Skipped
CodeQL / analyze (ruby) (push) Skipped
CodeQL / analyze-cpp (push) Skipped
FreeBSD / pre-check (push) Failing after 3s
FreeBSD / freebsd (14.4) (push) Skipped
FreeBSD / freebsd (15.0) (push) Skipped
Linux / pre-check (push) Failing after 4s
Linux / latest (apidoc) (push) Skipped
Linux / latest (clang, no, all) (push) Skipped
Linux / latest (clang, no, default) (push) Skipped
Linux / latest (clang, no, printf-builtin) (push) Skipped
Linux / latest (clang, no-dbg) (push) Skipped
Linux / latest (clang, no-testable-ke) (push) Skipped
Linux / latest (clang, yes, all) (push) Skipped
Linux / latest (clang, yes, default) (push) Skipped
Linux / latest (clang, yes, fuzzing) (push) Skipped
Linux / latest (coverage) (push) Skipped
Linux / latest (dist) (push) Skipped
Linux / latest (gcc, no, all) (push) Skipped
Linux / latest (gcc, no, default) (push) Skipped
Linux / latest (gcc, no, printf-builtin) (push) Skipped
Linux / latest (gcc, yes, all) (push) Skipped
Linux / latest (gcc, yes, default) (push) Skipped
Linux / latest (nm) (push) Skipped
Linux / latest (no-dbg) (push) Skipped
Linux / latest (no-testable-ke) (push) Skipped
Linux / latest (yes, ld) (push) Skipped
Linux / crypto (ubuntu-22.04, openssl-sys) (push) Skipped
Linux / crypto (ubuntu-latest, botan) (push) Skipped
Linux / crypto (ubuntu-latest, openssl-3) (push) Skipped
Linux / crypto (ubuntu-latest, openssl-4) (push) Skipped
Linux / crypto (ubuntu-latest, openssl-awslc) (push) Skipped
Linux / crypto (ubuntu-latest, openssl-sys) (push) Skipped
Linux / crypto (ubuntu-latest, wolfssl) (push) Skipped
Linux / older (clang, ubuntu-22.04, all) (push) Skipped
Linux / older (gcc, ubuntu-22.04, all) (push) Skipped
Linux / older (gcc, ubuntu-22.04, nm) (push) Skipped
Linux / alpine (push) Skipped
macOS / pre-check (push) Failing after 4s
macOS / macos (macos-14) (push) Skipped
macOS / macos (macos-latest) (push) Skipped
SonarCloud / pre-check (push) Failing after 3s
SonarCloud / sonarcloud (push) Skipped
TKM / pre-check (push) Failing after 3s
TKM / tkm (push) Skipped
Windows / pre-check (push) Failing after 4s
Windows / cross-compile (win32) (push) Skipped
Windows / cross-compile (win64) (push) Skipped
Windows / native (i686, mingw32, win32) (push) Skipped
Windows / native (x86_64, mingw64, win64) (push) Skipped
- проверка обновлений через API панели (get.shnt.top/api/v1): подписанный запрос (HMAC), манифест с подписью Ed25519; старый бакет — только запасной путь при сетевой ошибке - экраны обновления по макету: страница «Обновления» с каналом и кодом беты, шторки «Что нового», «Скачивание» (с отменой), «Обязательное обновление», «Не установлено» - отправка отчёта из журнала в панель: код отчёта, запись в «Событиях», письмо как запасной путь; к отчёту прикладываются результаты проверки сети (checks.json) и diag.log - исправлен вылет при скачивании обновления на Android 16.0 (setRequestPromotedOngoing есть только с API 36.1) - кнопки шторок переносят текст на вторую строку, однословные уменьшаются - задача Gradle uploadShx: сборка и загрузка APK в панель (scripts/upload-apk.mjs) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
108 lines
5.4 KiB
JavaScript
108 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// Загрузка собранного APK в панель SHX VPN с компьютера сборки. Нужен только Node 18+, без зависимостей.
|
|
//
|
|
// node upload-apk.mjs app-release.apk --type phone --channel beta [--publish] [--rollout 100] [--notes "Что нового"]
|
|
//
|
|
// Окружение:
|
|
// SHX_URL адрес консоли, например https://apk.shnt.top
|
|
// SHX_TOKEN токен CI из «Настройки → Токены» (upload — только черновик, publish — можно публиковать)
|
|
//
|
|
// versionName и versionCode берутся из output-metadata.json, который Gradle кладёт рядом с APK,
|
|
// либо из --version-name / --version-code.
|
|
//
|
|
// Шаги: билет на загрузку → файл напрямую в S3 по presigned PUT → сервер проверяет SHA-256 и оформляет версию.
|
|
import { execFileSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { parseArgs } from "node:util";
|
|
|
|
const { values: o, positionals } = parseArgs({
|
|
allowPositionals: true,
|
|
options: {
|
|
type: { type: "string" },
|
|
channel: { type: "string", default: "beta" },
|
|
"version-name": { type: "string" },
|
|
"version-code": { type: "string" },
|
|
notes: { type: "string", default: "" },
|
|
"notes-en": { type: "string", default: "" },
|
|
publish: { type: "boolean", default: false },
|
|
rollout: { type: "string", default: "100" },
|
|
help: { type: "boolean", short: "h", default: false },
|
|
},
|
|
});
|
|
|
|
const die = (msg) => { console.error("✗ " + msg); process.exit(1); };
|
|
if (o.help || positionals.length !== 1) {
|
|
console.log("node upload-apk.mjs <файл.apk> --type <id типа> [--channel beta|release] [--publish] [--rollout 1-100] [--notes текст] [--version-name 2.20.0 --version-code 220]");
|
|
process.exit(o.help ? 0 : 1);
|
|
}
|
|
|
|
// Windows: переменные, заданные через [Environment]::SetEnvironmentVariable(..., "User"), видны только новым
|
|
// процессам. Если терминал (или Gradle daemon) открыт раньше, читаем их из реестра пользователя.
|
|
function userEnv(name) {
|
|
if (process.env[name]) return process.env[name];
|
|
if (process.platform !== "win32") return "";
|
|
try {
|
|
const out = execFileSync("reg", ["query", "HKCU\\Environment", "/v", name], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
return /REG_(?:EXPAND_)?SZ\s+(.*)$/m.exec(out)?.[1]?.trim() ?? "";
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
const base = userEnv("SHX_URL").replace(/\/$/, "");
|
|
const token = userEnv("SHX_TOKEN");
|
|
if (!base) die("Задайте SHX_URL (адрес консоли)");
|
|
if (!token) die("Задайте SHX_TOKEN (Настройки → Токены в консоли)");
|
|
if (!o.type) die("Укажите --type (id типа приложения из раздела «Типы приложений»)");
|
|
if (!["beta", "release"].includes(o.channel)) die("--channel: beta или release");
|
|
const rolloutPct = Number(o.rollout);
|
|
if (!Number.isInteger(rolloutPct) || rolloutPct < 1 || rolloutPct > 100) die("--rollout: целое от 1 до 100");
|
|
|
|
const file = positionals[0];
|
|
if (!existsSync(file)) die("Нет файла " + file);
|
|
|
|
let versionName = o["version-name"];
|
|
let versionCode = o["version-code"] ? Number(o["version-code"]) : undefined;
|
|
const meta = join(dirname(file), "output-metadata.json");
|
|
if ((!versionName || !versionCode) && existsSync(meta)) {
|
|
const el = JSON.parse(readFileSync(meta, "utf8")).elements?.[0];
|
|
versionName ??= el?.versionName;
|
|
versionCode ??= el?.versionCode;
|
|
}
|
|
if (!versionName || !Number.isInteger(versionCode)) die("Не удалось определить версию: передайте --version-name и --version-code");
|
|
|
|
const bytes = readFileSync(file);
|
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
const mb = (statSync(file).size / 1048576).toFixed(1);
|
|
|
|
async function api(path, body) {
|
|
const r = await fetch(base + path, {
|
|
method: "POST",
|
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
const data = await r.json().catch(() => ({}));
|
|
if (!r.ok) die(`${path}: ${r.status} ${data?.error?.message ?? r.statusText}`);
|
|
return data;
|
|
}
|
|
|
|
console.log(`→ ${o.type} / ${o.channel} / ${versionName} (${versionCode}), ${mb} МБ, sha256 ${sha256.slice(0, 12)}…`);
|
|
|
|
const ticket = await api("/api/admin/versions/uploads", { typeId: o.type, channel: o.channel, versionName, versionCode });
|
|
if (bytes.length > ticket.maxBytes) die("Файл больше допустимого размера");
|
|
|
|
const put = await fetch(ticket.url, { method: ticket.method, headers: ticket.headers, body: bytes });
|
|
if (!put.ok) die(`Загрузка в хранилище: ${put.status} ${await put.text().catch(() => "")}`.slice(0, 300));
|
|
console.log("✓ файл в хранилище");
|
|
|
|
const v = await api("/api/admin/versions", {
|
|
uploadId: ticket.uploadId,
|
|
sha256,
|
|
notes: { ru: o.notes, en: o["notes-en"] },
|
|
publish: o.publish,
|
|
rolloutPct,
|
|
});
|
|
console.log(`✓ версия оформлена: ${v.versionName}, статус ${v.status}${o.publish ? `, раскатка ${rolloutPct}%` : " (черновик: опубликуйте в консоли)"}`);
|
|
console.log(` ${base}/versions/${v.id}`);
|