feat: Added offline AppImage-only release verification and built the...
- scripts/gitea-release.mjs - tests/gitea-release.test.ts - dist/LearnHarness-0.6.0-x86_64.AppImage - dist/latest-linux.yml - dist/SHA256SUMS - dist/linux-unpacked/resources/app-update.yml - dist/linux-unpacked/resources/server/standalone/server.js GSD context: - Milestone: M001 - Merge upstream v0.4.0 into the Gitea fork - Slice: S03 - Build and verify Linux desktop - Task: T01 - Added offline AppImage-only release verification and built the local Linux x64 LearnHarness 0.6.0 AppImage without publishing. GSD-Task: S03/T01
This commit is contained in:
+25
-12
@@ -18,9 +18,9 @@ export const REPOSITORY = "replicated/learnharness";
|
||||
export const UPDATE_URL = `${SERVER}/${REPOSITORY}/releases/download/latest`;
|
||||
const API = `${SERVER}/api/v1/repos/${REPOSITORY}`;
|
||||
|
||||
export function expectedLinuxAssets(version) {
|
||||
export function expectedLinuxAssets(version, { appimageOnly = false } = {}) {
|
||||
assert.match(version, /^\d+\.\d+\.\d+$/, "release version must be a stable semver");
|
||||
return [`LearnHarness-${version}-x86_64.AppImage`, `LearnHarness-${version}-amd64.deb`, "latest-linux.yml", "SHA256SUMS"];
|
||||
return [`LearnHarness-${version}-x86_64.AppImage`, ...(!appimageOnly ? [`LearnHarness-${version}-amd64.deb`] : []), "latest-linux.yml", "SHA256SUMS"];
|
||||
}
|
||||
export async function fileDigest(file, algorithm, encoding = "hex") {
|
||||
const hash = createHash(algorithm);
|
||||
@@ -28,10 +28,15 @@ export async function fileDigest(file, algorithm, encoding = "hex") {
|
||||
return hash.digest(encoding);
|
||||
}
|
||||
|
||||
export async function writeChecksums(directory, version) {
|
||||
const names = expectedLinuxAssets(version).filter((name) => name !== "SHA256SUMS");
|
||||
export async function writeChecksums(directory, version, options = {}) {
|
||||
const names = expectedLinuxAssets(version, options).filter((name) => name !== "SHA256SUMS");
|
||||
const lines = [];
|
||||
for (const name of names) lines.push(`${await fileDigest(path.join(directory, name), "sha256")} ${name}`);
|
||||
for (const name of names) {
|
||||
const file = path.join(directory, name);
|
||||
const stat = await fsp.lstat(file);
|
||||
assert(stat.isFile() && stat.size > 0, `${name} must be a nonempty regular file`);
|
||||
lines.push(`${await fileDigest(file, "sha256")} ${name}`);
|
||||
}
|
||||
await fsp.writeFile(path.join(directory, "SHA256SUMS"), lines.join("\n") + "\n");
|
||||
}
|
||||
|
||||
@@ -42,8 +47,8 @@ export async function verifyPackagedUpdater(directory) {
|
||||
assert(!config.owner && !config.repo, "packaged updater must not retain an upstream GitHub destination");
|
||||
}
|
||||
|
||||
export async function verifyLocal(directory, version) {
|
||||
const names = expectedLinuxAssets(version);
|
||||
export async function verifyLocal(directory, version, options = {}) {
|
||||
const names = expectedLinuxAssets(version, options);
|
||||
const inventory = new Map();
|
||||
for (const name of names) {
|
||||
const file = path.join(directory, name);
|
||||
@@ -51,7 +56,8 @@ export async function verifyLocal(directory, version) {
|
||||
assert(stat.isFile() && stat.size > 0, `${name} must be a nonempty regular file`);
|
||||
inventory.set(name, { name, size: stat.size, sha256: await fileDigest(file, "sha256") });
|
||||
}
|
||||
for (const [name, magic] of [[names[0], Buffer.from([0x7f, 0x45, 0x4c, 0x46])], [names[1], Buffer.from("!<arch>\n")]]) {
|
||||
for (const name of names.filter((name) => /\.(AppImage|deb)$/.test(name))) {
|
||||
const magic = name.endsWith(".AppImage") ? Buffer.from([0x7f, 0x45, 0x4c, 0x46]) : Buffer.from("!<arch>\n");
|
||||
const handle = await fsp.open(path.join(directory, name), "r");
|
||||
try { const header = Buffer.alloc(magic.length); await handle.read(header, 0, header.length, 0); assert(header.equals(magic), `${name} has an invalid installer header`); } finally { await handle.close(); }
|
||||
}
|
||||
@@ -149,11 +155,18 @@ async function upload(directory, version) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [command, directory = "dist"] = process.argv.slice(2);
|
||||
const [command, ...args] = process.argv.slice(2);
|
||||
const appimageOnly = args.includes("--appimage-only");
|
||||
assert(!appimageOnly || ["checksums", "verify"].includes(command), "--appimage-only is supported only by offline checksums and verify");
|
||||
assert(args.filter((arg) => arg === "--appimage-only").length <= 1, "duplicate --appimage-only option");
|
||||
assert(args.every((arg) => !arg.startsWith("--") || arg === "--appimage-only"), "unknown release option");
|
||||
const positional = args.filter((arg) => arg !== "--appimage-only");
|
||||
assert(positional.length <= 1, "expected at most one release directory");
|
||||
const directory = positional[0] ?? "dist";
|
||||
const { version } = JSON.parse(await fsp.readFile(path.join(ROOT, "package.json"), "utf8"));
|
||||
if (command === "checksums") await writeChecksums(directory, version);
|
||||
if (command === "checksums") await writeChecksums(directory, version, { appimageOnly });
|
||||
else if (command === "verify-package") await verifyPackagedUpdater(directory);
|
||||
else if (command === "verify") console.log(JSON.stringify(await verifyLocal(directory, version), null, 2));
|
||||
else if (command === "verify") console.log(JSON.stringify(await verifyLocal(directory, version, { appimageOnly }), null, 2));
|
||||
else if (command === "upload") await upload(directory, version);
|
||||
else if (command === "verify-remote") {
|
||||
const inventory = await verifyLocal(directory, version);
|
||||
@@ -161,7 +174,7 @@ async function main() {
|
||||
assert(release, "draft not found");
|
||||
await verifyRemote(release, inventory, authorization());
|
||||
console.log(`Verified draft: ${release.html_url}`);
|
||||
} else throw new Error("Usage: node scripts/gitea-release.mjs checksums|verify|upload|verify-remote [dist-directory], or verify-package [unpacked-app-directory]");
|
||||
} else throw new Error("Usage: node scripts/gitea-release.mjs checksums|verify [dist-directory] [--appimage-only], upload|verify-remote [dist-directory], or verify-package [unpacked-app-directory]");
|
||||
}
|
||||
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
|
||||
main().catch((error) => { console.error(error.message); process.exitCode = 1; });
|
||||
|
||||
Reference in New Issue
Block a user