mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
Adds a new `--to zip` (and `--to all`) tools-pack Windows build target that produces a portable `.zip` from the cached `win-unpacked` tree using the bundled 7z. The zip lays files at the archive root so users can extract it anywhere and launch `Open Design.exe` without going through the NSIS installer, addressing the no-install download request. Release plumbing is updated to publish the portable zip and its sha256 beside the existing installer on R2 for beta, preview, and stable channels (default on, gated by `WINDOWS_INCLUDE_ZIP`/`WIN_INCLUDE_ZIP`). The electron-updater `latest.yml` feed continues to point only at the installer; the zip is a manual-download convenience and is intentionally excluded from the in-app updater. Closes #1121 Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code) Co-authored-by: libertecode <libertecode@proton.me>
300 lines
10 KiB
TypeScript
300 lines
10 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
|
import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
import { basename, join, relative, sep } from "node:path";
|
|
|
|
function required(name) {
|
|
const value = process.env[name];
|
|
if (value == null || value.length === 0) {
|
|
throw new Error(`${name} is required`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function optional(name, fallback = "") {
|
|
const value = process.env[name];
|
|
return value == null || value.length === 0 ? fallback : value;
|
|
}
|
|
|
|
function bool(name) {
|
|
return process.env[name] === "true";
|
|
}
|
|
|
|
function normalizePath(value) {
|
|
return value.split(sep).join("/");
|
|
}
|
|
|
|
function publicUrl(prefix, name) {
|
|
return `${publicOrigin}/${prefix}/${name}`;
|
|
}
|
|
|
|
function contentType(name) {
|
|
if (name.endsWith(".dmg")) return "application/x-apple-diskimage";
|
|
if (name.endsWith(".zip")) return "application/zip";
|
|
if (name.endsWith(".exe")) return "application/vnd.microsoft.portable-executable";
|
|
if (name.endsWith(".AppImage")) return "application/octet-stream";
|
|
if (name.endsWith(".sha256")) return "text/plain; charset=utf-8";
|
|
if (name.endsWith(".yml") || name.endsWith(".yaml")) return "application/x-yaml; charset=utf-8";
|
|
if (name.endsWith(".json")) return "application/json; charset=utf-8";
|
|
if (name.endsWith(".html")) return "text/html; charset=utf-8";
|
|
if (name.endsWith(".log") || name.endsWith(".txt")) return "text/plain; charset=utf-8";
|
|
if (name.endsWith(".png")) return "image/png";
|
|
if (name.endsWith(".xml")) return "application/xml; charset=utf-8";
|
|
return "application/octet-stream";
|
|
}
|
|
|
|
function upload(filePath, objectKey, type, cacheControl) {
|
|
if (!existsSync(filePath) || !statSync(filePath).isFile()) {
|
|
throw new Error(`expected upload file not found: ${filePath}`);
|
|
}
|
|
execFileSync(
|
|
"aws",
|
|
[
|
|
"--endpoint-url",
|
|
endpointUrl,
|
|
"s3api",
|
|
"put-object",
|
|
"--bucket",
|
|
bucket,
|
|
"--key",
|
|
objectKey,
|
|
"--body",
|
|
filePath,
|
|
"--content-type",
|
|
type,
|
|
"--cache-control",
|
|
cacheControl,
|
|
"--no-cli-pager",
|
|
],
|
|
{ stdio: "inherit" },
|
|
);
|
|
}
|
|
|
|
function fileEntry(name, type) {
|
|
const filePath = join(releaseRoot, name);
|
|
const size = statSync(filePath).size;
|
|
const entry = {
|
|
contentType: type,
|
|
name,
|
|
size,
|
|
url: publicUrl(versionPrefix, name),
|
|
};
|
|
const checksumPath = join(releaseRoot, `${name}.sha256`);
|
|
if (existsSync(checksumPath)) {
|
|
entry.sha256Url = publicUrl(versionPrefix, `${name}.sha256`);
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
function uploadAsset(name) {
|
|
upload(join(releaseRoot, name), `${versionPrefix}/${name}`, contentType(name), "public, max-age=31536000, immutable");
|
|
}
|
|
|
|
function listFiles(root) {
|
|
if (!existsSync(root) || !statSync(root).isDirectory()) return [];
|
|
const files = [];
|
|
const visit = (dir) => {
|
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
const path = join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
visit(path);
|
|
} else if (entry.isFile()) {
|
|
files.push(path);
|
|
}
|
|
}
|
|
};
|
|
visit(root);
|
|
files.sort();
|
|
return files;
|
|
}
|
|
|
|
function uploadReport(reportDirectory) {
|
|
const files = listFiles(reportRoot);
|
|
if (files.length === 0) {
|
|
throw new Error(`expected ${platform} release report files in ${reportRoot}`);
|
|
}
|
|
const reportPrefix = `${versionPrefix}/report/${reportDirectory}`;
|
|
for (const file of files) {
|
|
const relativePath = normalizePath(relative(reportRoot, file));
|
|
upload(file, `${reportPrefix}/${relativePath}`, contentType(file), "public, max-age=31536000, immutable");
|
|
}
|
|
return {
|
|
fileCount: files.length,
|
|
type: "directory",
|
|
url: `${publicOrigin}/${reportPrefix}/`,
|
|
};
|
|
}
|
|
|
|
function setOutput(name, value) {
|
|
const outputPath = process.env.GITHUB_OUTPUT;
|
|
if (outputPath == null || outputPath.length === 0 || value == null) return;
|
|
appendFileSync(outputPath, `${name}=${value}\n`);
|
|
}
|
|
|
|
const platform = required("RELEASE_PLATFORM");
|
|
const releaseChannel = required("RELEASE_CHANNEL");
|
|
const releaseVersion = required("RELEASE_VERSION");
|
|
const bucket = required("CLOUDFLARE_R2_RELEASES_BUCKET");
|
|
const endpointUrl = required("CLOUDFLARE_R2_RELEASES_URL").replace(/\/+$/, "");
|
|
const publicOrigin = required("CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN").replace(/\/+$/, "");
|
|
const runnerTemp = required("RUNNER_TEMP");
|
|
const assetVersionSuffix = optional("ASSET_VERSION_SUFFIX");
|
|
const versionPrefix = optional("RELEASE_VERSION_PREFIX", `${releaseChannel}/versions/${releaseVersion}${assetVersionSuffix}`);
|
|
const latestPrefix = `${releaseChannel}/latest`;
|
|
const releaseRoot = optional("RELEASE_ROOT", join(runnerTemp, "release-assets"));
|
|
const manifestRoot = optional("PLATFORM_MANIFEST_ROOT", join(runnerTemp, "release-platform-manifests"));
|
|
|
|
let config;
|
|
if (platform === "mac") {
|
|
const suffix = assetVersionSuffix;
|
|
const dmg = `open-design-${releaseVersion}${suffix}-mac-arm64.dmg`;
|
|
const zip = `open-design-${releaseVersion}${suffix}-mac-arm64.zip`;
|
|
const artifactMode = optional("MAC_ARTIFACT_MODE", "dmg-and-zip");
|
|
const artifacts = { dmg: fileEntry(dmg, contentType(dmg)) };
|
|
const assetNames = [dmg, `${dmg}.sha256`];
|
|
let feed = null;
|
|
if (artifactMode !== "dmg-only") {
|
|
artifacts.zip = fileEntry(zip, contentType(zip));
|
|
assetNames.push(zip, `${zip}.sha256`, "latest-mac.yml");
|
|
feed = {
|
|
latestUrl: publicUrl(latestPrefix, "latest-mac.yml"),
|
|
name: "latest-mac.yml",
|
|
url: publicUrl(versionPrefix, "latest-mac.yml"),
|
|
};
|
|
}
|
|
config = {
|
|
arch: "arm64",
|
|
artifactMode,
|
|
artifacts,
|
|
assetNames,
|
|
feed,
|
|
key: "mac",
|
|
label: "macOS arm64",
|
|
reportDirectory: "mac",
|
|
signed: bool("RELEASE_SIGNED"),
|
|
};
|
|
} else if (platform === "win") {
|
|
const suffix = optional("WIN_ASSET_SUFFIX", assetVersionSuffix);
|
|
const installer = `open-design-${releaseVersion}${suffix}-win-x64-setup.exe`;
|
|
const portableZip = `open-design-${releaseVersion}${suffix}-win-x64-portable.zip`;
|
|
const includeZip = optional("WIN_INCLUDE_ZIP", "true") !== "false";
|
|
const artifacts = { installer: fileEntry(installer, contentType(installer)) };
|
|
const assetNames = [installer, `${installer}.sha256`, "latest.yml"];
|
|
if (includeZip) {
|
|
artifacts.portableZip = fileEntry(portableZip, contentType(portableZip));
|
|
assetNames.push(portableZip, `${portableZip}.sha256`);
|
|
}
|
|
config = {
|
|
arch: "x64",
|
|
artifacts,
|
|
assetNames,
|
|
feed: {
|
|
latestUrl: publicUrl(latestPrefix, "latest.yml"),
|
|
name: "latest.yml",
|
|
url: publicUrl(versionPrefix, "latest.yml"),
|
|
},
|
|
key: "win",
|
|
label: "Windows x64",
|
|
reportDirectory: "win",
|
|
signed: false,
|
|
};
|
|
} else if (platform === "linux") {
|
|
const suffix = optional("LINUX_ASSET_SUFFIX", assetVersionSuffix);
|
|
const appImage = `open-design-${releaseVersion}${suffix}-linux-x64.AppImage`;
|
|
config = {
|
|
arch: "x64",
|
|
artifacts: { appImage: fileEntry(appImage, contentType(appImage)) },
|
|
assetNames: [appImage, `${appImage}.sha256`],
|
|
feed: null,
|
|
key: "linux",
|
|
label: "Linux x64",
|
|
reportDirectory: "linux",
|
|
signed: false,
|
|
};
|
|
} else if (platform === "mac-intel") {
|
|
const suffix = optional("MAC_INTEL_ASSET_SUFFIX", assetVersionSuffix);
|
|
const dmg = `open-design-${releaseVersion}${suffix}-mac-x64.dmg`;
|
|
const zip = `open-design-${releaseVersion}${suffix}-mac-x64.zip`;
|
|
config = {
|
|
arch: "x64",
|
|
artifacts: {
|
|
dmg: fileEntry(dmg, contentType(dmg)),
|
|
zip: fileEntry(zip, contentType(zip)),
|
|
},
|
|
assetNames: [dmg, `${dmg}.sha256`, zip, `${zip}.sha256`],
|
|
feed: null,
|
|
key: "macIntel",
|
|
label: "macOS x64 (Intel)",
|
|
reportDirectory: null,
|
|
signed: bool("MAC_INTEL_SIGNED"),
|
|
};
|
|
} else {
|
|
throw new Error(`unsupported RELEASE_PLATFORM: ${platform}`);
|
|
}
|
|
|
|
const reportRoot = optional(
|
|
"REPORT_ROOT",
|
|
config.reportDirectory == null ? join(runnerTemp, "release-report", config.key) : join(runnerTemp, "release-report", config.reportDirectory),
|
|
);
|
|
|
|
for (const name of config.assetNames) {
|
|
uploadAsset(name);
|
|
if (name === "latest.yml" || name === "latest-mac.yml") {
|
|
upload(join(releaseRoot, name), `${latestPrefix}/${name}`, contentType(name), "public, max-age=60, must-revalidate");
|
|
}
|
|
}
|
|
|
|
const report = config.reportDirectory == null ? null : uploadReport(config.reportDirectory);
|
|
const now = new Date().toISOString();
|
|
const versionManifestUrl = publicUrl(versionPrefix, `platforms/${config.key}.json`);
|
|
const latestManifestUrl = publicUrl(latestPrefix, `platforms/${config.key}.json`);
|
|
const manifest = {
|
|
arch: config.arch,
|
|
artifacts: config.artifacts,
|
|
channel: releaseChannel,
|
|
enabled: true,
|
|
feed: config.feed,
|
|
generatedAt: now,
|
|
github: {
|
|
branch: process.env.GITHUB_REF_NAME ?? "",
|
|
commit: process.env.GITHUB_SHA ?? "",
|
|
repository: process.env.GITHUB_REPOSITORY ?? "",
|
|
runAttempt: Number(process.env.GITHUB_RUN_ATTEMPT ?? "0"),
|
|
runId: Number(process.env.GITHUB_RUN_ID ?? "0"),
|
|
workflow: process.env.GITHUB_WORKFLOW ?? "",
|
|
},
|
|
label: config.label,
|
|
platform,
|
|
platformKey: config.key,
|
|
r2: {
|
|
latestManifestUrl,
|
|
latestPrefix,
|
|
publicOrigin,
|
|
versionManifestUrl,
|
|
versionPrefix,
|
|
},
|
|
releaseVersion,
|
|
report,
|
|
signed: config.signed,
|
|
status: "published",
|
|
version: 1,
|
|
};
|
|
|
|
mkdirSync(manifestRoot, { recursive: true });
|
|
const manifestPath = join(manifestRoot, `${config.key}.json`);
|
|
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
upload(manifestPath, `${versionPrefix}/platforms/${config.key}.json`, "application/json; charset=utf-8", "public, max-age=31536000, immutable");
|
|
upload(manifestPath, `${latestPrefix}/platforms/${config.key}.json`, "application/json; charset=utf-8", "public, max-age=60, must-revalidate");
|
|
|
|
setOutput("platform_manifest_url", versionManifestUrl);
|
|
setOutput("platform_latest_manifest_url", latestManifestUrl);
|
|
for (const [artifactName, artifact] of Object.entries(config.artifacts)) {
|
|
setOutput(`${artifactName}_url`, artifact.url);
|
|
}
|
|
if (config.feed != null) {
|
|
setOutput("feed_url", config.feed.latestUrl);
|
|
}
|
|
if (report != null) {
|
|
setOutput("report_url", report.url);
|
|
}
|
|
|
|
console.log(`published ${config.label} beta assets to ${versionPrefix}`);
|