mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
chore: optimize CI and beta release runtime (#2231)
* chore(ci): add runtime trace summaries * chore(ci): tighten measured workspace steps * chore(release): tighten beta setup steps * chore(release): slim beta windows smoke * chore(ci): shard daemon tests * chore(ci): harden runtime trace lookup * chore(release): avoid mac pnpm cache in beta * chore(ci): split critical playwright checks * chore(release): publish beta platforms from builders * test(e2e): update beta release workflow expectation * chore(ci): stop gating PRs on nix check * fix(release): keep beta latest complete
This commit is contained in:
parent
320bd4c303
commit
bb13eee765
10 changed files with 1054 additions and 204 deletions
188
.github/scripts/release/r2/publish-beta-metadata.ts
vendored
Normal file
188
.github/scripts/release/r2/publish-beta-metadata.ts
vendored
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { join } 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 enabled(name) {
|
||||||
|
return process.env[name] === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
function upload(filePath, objectKey, contentType, cacheControl) {
|
||||||
|
execFileSync(
|
||||||
|
"aws",
|
||||||
|
[
|
||||||
|
"--endpoint-url",
|
||||||
|
endpointUrl,
|
||||||
|
"s3api",
|
||||||
|
"put-object",
|
||||||
|
"--bucket",
|
||||||
|
bucket,
|
||||||
|
"--key",
|
||||||
|
objectKey,
|
||||||
|
"--body",
|
||||||
|
filePath,
|
||||||
|
"--content-type",
|
||||||
|
contentType,
|
||||||
|
"--cache-control",
|
||||||
|
cacheControl,
|
||||||
|
"--no-cli-pager",
|
||||||
|
],
|
||||||
|
{ stdio: "inherit" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function publicUrl(prefix, name) {
|
||||||
|
return `${publicOrigin}/${prefix}/${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOutput(name, value) {
|
||||||
|
const outputPath = process.env.GITHUB_OUTPUT;
|
||||||
|
if (outputPath == null || outputPath.length === 0 || value == null) return;
|
||||||
|
appendFileSync(outputPath, `${name}=${value}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readManifest(key) {
|
||||||
|
const path = join(manifestRoot, `${key}.json`);
|
||||||
|
if (!existsSync(path)) return null;
|
||||||
|
return JSON.parse(readFileSync(path, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 releaseChannel = required("RELEASE_CHANNEL");
|
||||||
|
if (releaseChannel !== "beta") {
|
||||||
|
throw new Error(`publish-beta-metadata only supports beta, got ${releaseChannel}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseVersion = required("RELEASE_VERSION");
|
||||||
|
const assetVersionSuffix = optional("ASSET_VERSION_SUFFIX");
|
||||||
|
const versionPrefix = optional("RELEASE_VERSION_PREFIX", `${releaseChannel}/versions/${releaseVersion}${assetVersionSuffix}`);
|
||||||
|
const latestPrefix = `${releaseChannel}/latest`;
|
||||||
|
const manifestRoot = optional("PLATFORM_MANIFEST_ROOT", join(runnerTemp, "release-platform-manifests"));
|
||||||
|
|
||||||
|
const platformDefs = [
|
||||||
|
{ env: "ENABLE_MAC", key: "mac", label: "macOS arm64", result: optional("MAC_RESULT", "skipped") },
|
||||||
|
{ env: "ENABLE_WIN", key: "win", label: "Windows x64", result: optional("WIN_RESULT", "skipped") },
|
||||||
|
{ env: "ENABLE_LINUX", key: "linux", label: "Linux x64", result: optional("LINUX_RESULT", "skipped") },
|
||||||
|
{ env: "ENABLE_MAC_INTEL", key: "macIntel", label: "macOS x64 (Intel)", result: optional("MAC_INTEL_RESULT", "skipped") },
|
||||||
|
];
|
||||||
|
|
||||||
|
const platforms = {};
|
||||||
|
const expectedPlatforms = [];
|
||||||
|
const readyPlatforms = [];
|
||||||
|
const failedPlatforms = [];
|
||||||
|
|
||||||
|
for (const def of platformDefs) {
|
||||||
|
if (!enabled(def.env)) continue;
|
||||||
|
expectedPlatforms.push(def.key);
|
||||||
|
const manifest = readManifest(def.key);
|
||||||
|
if (manifest != null && def.result === "success") {
|
||||||
|
platforms[def.key] = {
|
||||||
|
...manifest,
|
||||||
|
enabled: true,
|
||||||
|
status: "published",
|
||||||
|
};
|
||||||
|
readyPlatforms.push(def.key);
|
||||||
|
} else {
|
||||||
|
const status = def.result === "success" ? "missing" : "failed";
|
||||||
|
platforms[def.key] = {
|
||||||
|
enabled: true,
|
||||||
|
label: def.label,
|
||||||
|
result: def.result,
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
failedPlatforms.push(def.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let releaseState = "failed";
|
||||||
|
if (expectedPlatforms.length > 0 && readyPlatforms.length === expectedPlatforms.length) {
|
||||||
|
releaseState = "complete";
|
||||||
|
} else if (readyPlatforms.length > 0) {
|
||||||
|
releaseState = "partial";
|
||||||
|
}
|
||||||
|
|
||||||
|
const reportUrl = publicUrl(versionPrefix, "report/");
|
||||||
|
const latestMetadataUpdated = releaseState === "complete";
|
||||||
|
const metadata = {
|
||||||
|
assetVersionSuffix,
|
||||||
|
baseVersion: required("BASE_VERSION"),
|
||||||
|
betaNumber: Number(releaseVersion.split("-beta.")[1]),
|
||||||
|
betaVersion: releaseVersion,
|
||||||
|
channel: releaseChannel,
|
||||||
|
expectedPlatforms,
|
||||||
|
failedPlatforms,
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
github: {
|
||||||
|
branch: required("BRANCH_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 ?? "",
|
||||||
|
},
|
||||||
|
platforms,
|
||||||
|
r2: {
|
||||||
|
latestMetadataUrl: publicUrl(latestPrefix, "metadata.json"),
|
||||||
|
latestMetadataUpdated,
|
||||||
|
latestPrefix,
|
||||||
|
publicOrigin,
|
||||||
|
report: {
|
||||||
|
type: "directory",
|
||||||
|
url: reportUrl,
|
||||||
|
},
|
||||||
|
reportUrl,
|
||||||
|
reportZipUrl: null,
|
||||||
|
versionMetadataUrl: publicUrl(versionPrefix, "metadata.json"),
|
||||||
|
versionPrefix,
|
||||||
|
},
|
||||||
|
readyPlatforms,
|
||||||
|
releaseState,
|
||||||
|
signed: process.env.RELEASE_SIGNED === "true",
|
||||||
|
stateSource: required("STATE_SOURCE"),
|
||||||
|
version: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const metadataPath = join(runnerTemp, "release-beta-metadata.json");
|
||||||
|
writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
||||||
|
upload(metadataPath, `${versionPrefix}/metadata.json`, "application/json; charset=utf-8", "public, max-age=31536000, immutable");
|
||||||
|
if (latestMetadataUpdated) {
|
||||||
|
upload(metadataPath, `${latestPrefix}/metadata.json`, "application/json; charset=utf-8", "public, max-age=60, must-revalidate");
|
||||||
|
} else {
|
||||||
|
console.log(`left ${metadata.r2.latestMetadataUrl} unchanged because releaseState=${releaseState}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setOutput("metadata_url", metadata.r2.latestMetadataUrl);
|
||||||
|
setOutput("latest_metadata_updated", String(latestMetadataUpdated));
|
||||||
|
setOutput("version_metadata_url", metadata.r2.versionMetadataUrl);
|
||||||
|
setOutput("version_prefix", versionPrefix);
|
||||||
|
setOutput("report_url", reportUrl);
|
||||||
|
setOutput("release_state", releaseState);
|
||||||
|
|
||||||
|
for (const [key, platform] of Object.entries(platforms)) {
|
||||||
|
if (platform.status !== "published") continue;
|
||||||
|
for (const [artifactName, artifact] of Object.entries(platform.artifacts ?? {})) {
|
||||||
|
setOutput(`${key}_${artifactName}_url`, artifact.url);
|
||||||
|
}
|
||||||
|
if (platform.feed?.latestUrl != null) {
|
||||||
|
setOutput(`${key}_feed_url`, platform.feed.latestUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirSync(join(runnerTemp, "release-metadata"), { recursive: true });
|
||||||
|
writeFileSync(join(runnerTemp, "release-metadata", "metadata.json"), `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
||||||
|
console.log(`published beta version metadata (${releaseState}) to ${metadata.r2.versionMetadataUrl}`);
|
||||||
292
.github/scripts/release/r2/publish-platform.ts
vendored
Normal file
292
.github/scripts/release/r2/publish-platform.ts
vendored
Normal file
|
|
@ -0,0 +1,292 @@
|
||||||
|
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`;
|
||||||
|
config = {
|
||||||
|
arch: "x64",
|
||||||
|
artifacts: { installer: fileEntry(installer, contentType(installer)) },
|
||||||
|
assetNames: [installer, `${installer}.sha256`, "latest.yml"],
|
||||||
|
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}`);
|
||||||
100
.github/scripts/release/r2/summary-beta.ts
vendored
Normal file
100
.github/scripts/release/r2/summary-beta.ts
vendored
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
function required(name) {
|
||||||
|
const value = process.env[name];
|
||||||
|
if (value == null || value.length === 0) {
|
||||||
|
throw new Error(`${name} is required`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchJson(url) {
|
||||||
|
const response = await fetch(`${url}${url.includes("?") ? "&" : "?"}run=${process.env.GITHUB_RUN_ID ?? "local"}`, {
|
||||||
|
headers: { "Cache-Control": "no-cache" },
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`GET ${url} failed with HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function md(value) {
|
||||||
|
return String(value ?? "-").replaceAll("|", "\\|").replaceAll("\n", " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function code(value) {
|
||||||
|
return value == null || value === "" ? "-" : `\`${md(value)}\``;
|
||||||
|
}
|
||||||
|
|
||||||
|
function link(label, url) {
|
||||||
|
return url == null || url === "" ? "-" : `[${md(label)}](${url})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function linkList(items) {
|
||||||
|
const links = items
|
||||||
|
.filter((item) => item.url != null && item.url !== "")
|
||||||
|
.map((item) => link(item.label, item.url));
|
||||||
|
return links.length === 0 ? "-" : links.join("<br>");
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = await fetchJson(required("R2_METADATA_URL"));
|
||||||
|
const overviewRows = [
|
||||||
|
["Channel", code(metadata.channel)],
|
||||||
|
["Version", code(metadata.betaVersion)],
|
||||||
|
["Release state", code(metadata.releaseState)],
|
||||||
|
["Ready platforms", code((metadata.readyPlatforms ?? []).join(", "))],
|
||||||
|
["Expected platforms", code((metadata.expectedPlatforms ?? []).join(", "))],
|
||||||
|
["State source", code(metadata.stateSource)],
|
||||||
|
];
|
||||||
|
|
||||||
|
const overviewTable = [
|
||||||
|
"| Field | Value |",
|
||||||
|
"| --- | --- |",
|
||||||
|
...overviewRows.map(([field, value]) => `| ${md(field)} | ${value} |`),
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
const releaseLinks = [
|
||||||
|
["Latest metadata", metadata.r2?.latestMetadataUrl],
|
||||||
|
["Version metadata", metadata.r2?.versionMetadataUrl],
|
||||||
|
["Report root", metadata.r2?.reportUrl],
|
||||||
|
]
|
||||||
|
.filter(([, url]) => url != null)
|
||||||
|
.map(([label, url]) => `- ${link(label, url)}`)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const platformLabels = {
|
||||||
|
linux: "Linux x64",
|
||||||
|
mac: "macOS arm64",
|
||||||
|
macIntel: "macOS x64 (Intel)",
|
||||||
|
win: "Windows x64",
|
||||||
|
};
|
||||||
|
const platformRows = Object.entries(platformLabels).map(([key, labelText]) => {
|
||||||
|
const platform = metadata.platforms?.[key];
|
||||||
|
if (platform == null) {
|
||||||
|
return [labelText, "Skipped", "-", "-", "-"];
|
||||||
|
}
|
||||||
|
const artifacts = platform.artifacts ?? {};
|
||||||
|
return [
|
||||||
|
labelText,
|
||||||
|
platform.status ?? "-",
|
||||||
|
linkList(Object.entries(artifacts).map(([name, artifact]) => ({ label: name, url: artifact.url }))),
|
||||||
|
link(platform.feed?.name ?? "feed", platform.feed?.latestUrl),
|
||||||
|
link("report", platform.report?.url),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
const platformTable = [
|
||||||
|
"| Platform | Status | Assets | Feed | Report |",
|
||||||
|
"| --- | --- | --- | --- | --- |",
|
||||||
|
...platformRows.map((row) => `| ${row.map(md).join(" | ")} |`),
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
console.log(`## Beta release summary
|
||||||
|
|
||||||
|
${overviewTable}
|
||||||
|
|
||||||
|
### Release links
|
||||||
|
|
||||||
|
${releaseLinks || "-"}
|
||||||
|
|
||||||
|
### Platform assets
|
||||||
|
|
||||||
|
${platformTable}
|
||||||
|
`);
|
||||||
150
.github/scripts/release/r2/verify-beta-metadata.ts
vendored
Normal file
150
.github/scripts/release/r2/verify-beta-metadata.ts
vendored
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
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 enabled(name) {
|
||||||
|
return process.env[name] === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchText(url) {
|
||||||
|
const response = await fetch(`${url}${url.includes("?") ? "&" : "?"}run=${process.env.GITHUB_RUN_ID ?? "local"}`, {
|
||||||
|
headers: { "Cache-Control": "no-cache" },
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`GET ${url} failed with HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
return await response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function head(url) {
|
||||||
|
const response = await fetch(url, { method: "HEAD" });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HEAD ${url} failed with HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinUrl(base, path) {
|
||||||
|
return `${base.replace(/\/+$/, "")}/${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportFilesFor(key) {
|
||||||
|
if (key === "mac") {
|
||||||
|
return [
|
||||||
|
"manifest.json",
|
||||||
|
"screenshots/open-design-mac-smoke.png",
|
||||||
|
"suite-result.json",
|
||||||
|
"tools-pack.json",
|
||||||
|
"tools-pack.log",
|
||||||
|
"vitest.log",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (key === "win") {
|
||||||
|
return [
|
||||||
|
"manifest.json",
|
||||||
|
"screenshots/open-design-win-smoke.png",
|
||||||
|
"suite-result.json",
|
||||||
|
"tools-pack.json",
|
||||||
|
"vitest.log",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (key === "linux") {
|
||||||
|
return ["manifest.json", "screenshots/open-design-linux-smoke.png", "vitest.log"];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyReport(def, platform) {
|
||||||
|
const expectedFiles = reportFilesFor(def.key);
|
||||||
|
if (expectedFiles.length === 0) return;
|
||||||
|
if (platform.report == null || platform.report.type !== "directory" || platform.report.url == null) {
|
||||||
|
throw new Error(`${def.key} is missing release report metadata`);
|
||||||
|
}
|
||||||
|
if (typeof platform.report.fileCount !== "number" || platform.report.fileCount <= 0) {
|
||||||
|
throw new Error(`${def.key} release report has no files`);
|
||||||
|
}
|
||||||
|
for (const file of expectedFiles) {
|
||||||
|
await head(joinUrl(platform.report.url, file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataUrl = required("R2_METADATA_URL");
|
||||||
|
const releaseVersion = required("RELEASE_VERSION");
|
||||||
|
const metadata = JSON.parse(await fetchText(metadataUrl));
|
||||||
|
|
||||||
|
if (metadata.channel !== "beta") {
|
||||||
|
throw new Error(`unexpected metadata channel: ${metadata.channel}`);
|
||||||
|
}
|
||||||
|
if (metadata.betaVersion !== releaseVersion) {
|
||||||
|
throw new Error(`unexpected metadata betaVersion: ${metadata.betaVersion}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const platformDefs = [
|
||||||
|
{ env: "ENABLE_MAC", key: "mac", result: optional("MAC_RESULT", "skipped") },
|
||||||
|
{ env: "ENABLE_WIN", key: "win", result: optional("WIN_RESULT", "skipped") },
|
||||||
|
{ env: "ENABLE_LINUX", key: "linux", result: optional("LINUX_RESULT", "skipped") },
|
||||||
|
{ env: "ENABLE_MAC_INTEL", key: "macIntel", result: optional("MAC_INTEL_RESULT", "skipped") },
|
||||||
|
];
|
||||||
|
const expected = platformDefs.filter((def) => enabled(def.env));
|
||||||
|
const expectedKeys = expected.map((def) => def.key).sort();
|
||||||
|
const metadataExpected = [...(metadata.expectedPlatforms ?? [])].sort();
|
||||||
|
if (JSON.stringify(expectedKeys) !== JSON.stringify(metadataExpected)) {
|
||||||
|
throw new Error(`unexpected expectedPlatforms: ${JSON.stringify(metadata.expectedPlatforms)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedReady = expected.filter((def) => def.result === "success").map((def) => def.key).sort();
|
||||||
|
const metadataReady = [...(metadata.readyPlatforms ?? [])].sort();
|
||||||
|
if (JSON.stringify(expectedReady) !== JSON.stringify(metadataReady)) {
|
||||||
|
throw new Error(`unexpected readyPlatforms: ${JSON.stringify(metadata.readyPlatforms)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedReleaseState =
|
||||||
|
expectedReady.length === expected.length ? "complete" : expectedReady.length > 0 ? "partial" : "failed";
|
||||||
|
if (metadata.releaseState !== expectedReleaseState) {
|
||||||
|
throw new Error(`unexpected releaseState: ${metadata.releaseState}; expected ${expectedReleaseState}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const def of expected) {
|
||||||
|
const platform = metadata.platforms?.[def.key];
|
||||||
|
if (platform == null) {
|
||||||
|
throw new Error(`metadata missing platform ${def.key}`);
|
||||||
|
}
|
||||||
|
if (def.result !== "success") {
|
||||||
|
if (platform.status !== "failed" && platform.status !== "missing") {
|
||||||
|
throw new Error(`unexpected failed platform status for ${def.key}: ${platform.status}`);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (platform.status !== "published") {
|
||||||
|
throw new Error(`unexpected platform status for ${def.key}: ${platform.status}`);
|
||||||
|
}
|
||||||
|
for (const artifact of Object.values(platform.artifacts ?? {})) {
|
||||||
|
await head(artifact.url);
|
||||||
|
if (artifact.sha256Url != null) {
|
||||||
|
await head(artifact.sha256Url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (platform.feed?.latestUrl != null) {
|
||||||
|
const feed = await fetchText(platform.feed.latestUrl);
|
||||||
|
if (!feed.includes(`version: "${releaseVersion}"`)) {
|
||||||
|
throw new Error(`${def.key} feed does not reference ${releaseVersion}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (platform.r2?.versionManifestUrl != null) {
|
||||||
|
await head(platform.r2.versionManifestUrl);
|
||||||
|
}
|
||||||
|
if (platform.r2?.latestManifestUrl != null) {
|
||||||
|
await head(platform.r2.latestManifestUrl);
|
||||||
|
}
|
||||||
|
await verifyReport(def, platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`verified beta metadata ${metadataUrl} (${metadata.releaseState})`);
|
||||||
113
.github/workflows/ci.yml
vendored
113
.github/workflows/ci.yml
vendored
|
|
@ -14,7 +14,9 @@ on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ci-${{ github.event.pull_request.number || github.ref }}
|
group: ci-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
|
@ -36,13 +38,10 @@ jobs:
|
||||||
workspace_validation_required: ${{ steps.detect.outputs.workspace_validation_required }}
|
workspace_validation_required: ${{ steps.detect.outputs.workspace_validation_required }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6.0.2
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Detect workspace and app test scopes
|
- name: Detect workspace and app test scopes
|
||||||
id: detect
|
id: detect
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
@ -52,7 +51,9 @@ jobs:
|
||||||
tools_pack_tests_required=false
|
tools_pack_tests_required=false
|
||||||
workspace_validation_required=false
|
workspace_validation_required=false
|
||||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||||
git diff --name-only "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}" > "$RUNNER_TEMP/changed-files.txt"
|
gh api --paginate \
|
||||||
|
"repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" \
|
||||||
|
--jq '.[].filename' > "$RUNNER_TEMP/changed-files.txt"
|
||||||
while IFS= read -r file; do
|
while IFS= read -r file; do
|
||||||
if [[ "$file" == "apps/daemon/"* || "$file" == "packages/contracts/"* || "$file" == "packages/platform/"* || "$file" == "packages/sidecar/"* || "$file" == "packages/sidecar-proto/"* ]]; then
|
if [[ "$file" == "apps/daemon/"* || "$file" == "packages/contracts/"* || "$file" == "packages/platform/"* || "$file" == "packages/sidecar/"* || "$file" == "packages/sidecar-proto/"* ]]; then
|
||||||
daemon_tests_required=true
|
daemon_tests_required=true
|
||||||
|
|
@ -172,7 +173,9 @@ jobs:
|
||||||
pnpm --filter @open-design/web build:sidecar
|
pnpm --filter @open-design/web build:sidecar
|
||||||
|
|
||||||
- name: Typecheck workspaces
|
- name: Typecheck workspaces
|
||||||
run: pnpm -r --workspace-concurrency=4 --if-present run typecheck
|
run: |
|
||||||
|
pnpm -r --filter '!open-design' --filter '!@open-design/landing-page' --workspace-concurrency=4 --if-present run typecheck
|
||||||
|
pnpm exec tsc -p scripts/tsconfig.json --noEmit
|
||||||
|
|
||||||
- name: Check repository layout policies
|
- name: Check repository layout policies
|
||||||
run: pnpm guard
|
run: pnpm guard
|
||||||
|
|
@ -269,11 +272,15 @@ jobs:
|
||||||
fi
|
fi
|
||||||
|
|
||||||
daemon_workspace_tests:
|
daemon_workspace_tests:
|
||||||
name: Daemon workspace tests
|
name: Daemon workspace tests (${{ matrix.shard }}/2)
|
||||||
needs: [change_scopes]
|
needs: [change_scopes]
|
||||||
if: ${{ needs.change_scopes.outputs.daemon_tests_required == 'true' }}
|
if: ${{ needs.change_scopes.outputs.daemon_tests_required == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
shard: [1, 2]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|
@ -308,7 +315,7 @@ jobs:
|
||||||
run: pnpm --filter @open-design/daemon build
|
run: pnpm --filter @open-design/daemon build
|
||||||
|
|
||||||
- name: Daemon workspace tests
|
- name: Daemon workspace tests
|
||||||
run: pnpm --filter @open-design/daemon test
|
run: pnpm --filter @open-design/daemon exec vitest run -c vitest.config.ts --shard=${{ matrix.shard }}/2
|
||||||
|
|
||||||
web_workspace_tests:
|
web_workspace_tests:
|
||||||
name: Web workspace tests
|
name: Web workspace tests
|
||||||
|
|
@ -429,11 +436,21 @@ jobs:
|
||||||
run: pnpm --filter @open-design/e2e test
|
run: pnpm --filter @open-design/e2e test
|
||||||
|
|
||||||
ui_e2e_critical:
|
ui_e2e_critical:
|
||||||
name: Playwright critical
|
name: Playwright critical (${{ matrix.group }})
|
||||||
needs: [change_scopes]
|
needs: [change_scopes]
|
||||||
if: ${{ needs.change_scopes.outputs.workspace_validation_required == 'true' }}
|
if: ${{ needs.change_scopes.outputs.workspace_validation_required == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- group: core
|
||||||
|
grep_flag: --grep-invert
|
||||||
|
grep_pattern: home starters|home hero
|
||||||
|
- group: starters
|
||||||
|
grep_flag: --grep
|
||||||
|
grep_pattern: home starters|home hero
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|
@ -491,7 +508,7 @@ jobs:
|
||||||
- name: Playwright critical
|
- name: Playwright critical
|
||||||
run: |
|
run: |
|
||||||
pnpm -C e2e exec tsx scripts/playwright.ts clean
|
pnpm -C e2e exec tsx scripts/playwright.ts clean
|
||||||
pnpm --filter @open-design/e2e run test:ui:critical
|
pnpm -C e2e exec playwright test -c playwright.config.ts ${{ matrix.grep_flag }} '${{ matrix.grep_pattern }}' ui/critical-smoke.test.ts ui/entry-chrome-flows.test.ts
|
||||||
|
|
||||||
build_workspaces:
|
build_workspaces:
|
||||||
name: Build workspaces
|
name: Build workspaces
|
||||||
|
|
@ -541,7 +558,7 @@ jobs:
|
||||||
# current workspace is small enough that safer logs and fewer shared-FS
|
# current workspace is small enough that safer logs and fewer shared-FS
|
||||||
# races outweigh the lost parallelism; revisit if the package count grows.
|
# races outweigh the lost parallelism; revisit if the package count grows.
|
||||||
- name: Build workspaces
|
- name: Build workspaces
|
||||||
run: pnpm -r --workspace-concurrency=1 --if-present run build
|
run: pnpm -r --filter '!@open-design/landing-page' --workspace-concurrency=1 --if-present run build
|
||||||
|
|
||||||
validate:
|
validate:
|
||||||
name: Validate workspace
|
name: Validate workspace
|
||||||
|
|
@ -569,3 +586,75 @@ jobs:
|
||||||
echo "$failures"
|
echo "$failures"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
runtime_trace:
|
||||||
|
name: Runtime trace
|
||||||
|
needs:
|
||||||
|
- validate
|
||||||
|
if: ${{ always() && github.event_name == 'workflow_dispatch' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Summarize workflow runtime
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
RUN_ID: ${{ github.run_id }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
run_json="$RUNNER_TEMP/run.json"
|
||||||
|
gh run view "$RUN_ID" --repo "$GITHUB_REPOSITORY" --json conclusion,createdAt,databaseId,displayTitle,event,headBranch,jobs,updatedAt,url > "$run_json"
|
||||||
|
jq -r '
|
||||||
|
def parse_ts: sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601;
|
||||||
|
def seconds($start; $end):
|
||||||
|
if ($start and $end) then (($end | parse_ts) - ($start | parse_ts)) else null end;
|
||||||
|
def fmt($seconds):
|
||||||
|
if $seconds == null then "n/a"
|
||||||
|
elif $seconds >= 60 then "\(((($seconds / 60) * 10 | round) / 10))m"
|
||||||
|
else "\(($seconds | round))s"
|
||||||
|
end;
|
||||||
|
def row($cells): "| \($cells | join(" | ")) |";
|
||||||
|
|
||||||
|
.jobs as $jobs |
|
||||||
|
[
|
||||||
|
"## Runtime trace",
|
||||||
|
"",
|
||||||
|
"Run: [\(.displayTitle)](\(.url))",
|
||||||
|
"Event: `\(.event)`",
|
||||||
|
"Branch: `\(.headBranch)`",
|
||||||
|
"Elapsed: \(fmt(seconds(.createdAt; .updatedAt)))",
|
||||||
|
"",
|
||||||
|
"### Jobs",
|
||||||
|
"| Job | Result | Duration | Slowest step |",
|
||||||
|
"| --- | --- | ---: | --- |",
|
||||||
|
(
|
||||||
|
$jobs
|
||||||
|
| sort_by(seconds(.startedAt; .completedAt) // 0)
|
||||||
|
| reverse
|
||||||
|
| .[]
|
||||||
|
| select(.conclusion != "skipped")
|
||||||
|
| (
|
||||||
|
[(.steps // [])[] | select(.startedAt and .completedAt and .conclusion != "skipped") | {name, duration: seconds(.startedAt; .completedAt)}]
|
||||||
|
| max_by(.duration // 0)
|
||||||
|
) as $slow
|
||||||
|
| row([.name, (.conclusion // .status), fmt(seconds(.startedAt; .completedAt)), "\($slow.name // "n/a") (\(fmt($slow.duration)))"])
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
"### Slowest steps",
|
||||||
|
"| Step | Job | Duration |",
|
||||||
|
"| --- | --- | ---: |",
|
||||||
|
(
|
||||||
|
[
|
||||||
|
$jobs[] as $job
|
||||||
|
| ($job.steps // [])[]
|
||||||
|
| select(.startedAt and .completedAt and .conclusion != "skipped")
|
||||||
|
| {job: $job.name, name, duration: seconds(.startedAt; .completedAt)}
|
||||||
|
]
|
||||||
|
| sort_by(.duration // 0)
|
||||||
|
| reverse
|
||||||
|
| .[0:20][]
|
||||||
|
| row([.name, .job, fmt(.duration)])
|
||||||
|
)
|
||||||
|
][]
|
||||||
|
' "$run_json" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
|
||||||
40
.github/workflows/nix-check.yml
vendored
40
.github/workflows/nix-check.yml
vendored
|
|
@ -24,27 +24,7 @@ on:
|
||||||
- .github/ISSUE_TEMPLATE/**
|
- .github/ISSUE_TEMPLATE/**
|
||||||
- .github/PULL_REQUEST_TEMPLATE.md
|
- .github/PULL_REQUEST_TEMPLATE.md
|
||||||
- .github/CODEOWNERS
|
- .github/CODEOWNERS
|
||||||
pull_request:
|
workflow_dispatch:
|
||||||
paths-ignore:
|
|
||||||
- '**/*.md'
|
|
||||||
- '**/*.mdx'
|
|
||||||
- '**/*.txt'
|
|
||||||
- LICENSE
|
|
||||||
- .gitignore
|
|
||||||
- .editorconfig
|
|
||||||
- .vscode/**
|
|
||||||
- .idea/**
|
|
||||||
- docs/**
|
|
||||||
- assets/**
|
|
||||||
- '**/*.png'
|
|
||||||
- '**/*.jpg'
|
|
||||||
- '**/*.jpeg'
|
|
||||||
- '**/*.gif'
|
|
||||||
- '**/*.svg'
|
|
||||||
- '**/*.webp'
|
|
||||||
- .github/ISSUE_TEMPLATE/**
|
|
||||||
- .github/PULL_REQUEST_TEMPLATE.md
|
|
||||||
- .github/CODEOWNERS
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
@ -75,21 +55,3 @@ jobs:
|
||||||
|
|
||||||
- name: nix flake check
|
- name: nix flake check
|
||||||
run: nix flake check --print-build-logs --keep-going
|
run: nix flake check --print-build-logs --keep-going
|
||||||
|
|
||||||
- name: nix build .#daemon
|
|
||||||
id: build-daemon
|
|
||||||
run: nix build .#daemon --print-build-logs --log-format raw |& tee daemon-build.log
|
|
||||||
|
|
||||||
- name: nix build .#web
|
|
||||||
id: build-web
|
|
||||||
run: nix build .#web --print-build-logs --log-format raw |& tee web-build.log
|
|
||||||
|
|
||||||
- name: Upload build logs (on failure)
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: nix-build-logs
|
|
||||||
path: |
|
|
||||||
daemon-build.log
|
|
||||||
web-build.log
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
|
||||||
316
.github/workflows/release-beta.yml
vendored
316
.github/workflows/release-beta.yml
vendored
|
|
@ -101,8 +101,6 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v6.0.2
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v5
|
uses: pnpm/action-setup@v5
|
||||||
|
|
@ -290,11 +288,28 @@ jobs:
|
||||||
TOOLS_PACK_NAMESPACE: release-beta
|
TOOLS_PACK_NAMESPACE: release-beta
|
||||||
run: bash .github/scripts/release/assets/mac.sh
|
run: bash .github/scripts/release/assets/mac.sh
|
||||||
|
|
||||||
- name: Upload mac release bundle
|
- name: Publish beta mac assets to R2
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_RELEASES_AK }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_RELEASES_SK }}
|
||||||
|
AWS_DEFAULT_REGION: auto
|
||||||
|
AWS_EC2_METADATA_DISABLED: "true"
|
||||||
|
CLOUDFLARE_R2_RELEASES_BUCKET: ${{ secrets.CLOUDFLARE_R2_RELEASES_BUCKET }}
|
||||||
|
CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }}
|
||||||
|
CLOUDFLARE_R2_RELEASES_URL: ${{ secrets.CLOUDFLARE_R2_RELEASES_URL }}
|
||||||
|
ASSET_VERSION_SUFFIX: ${{ needs.metadata.outputs.asset_version_suffix }}
|
||||||
|
MAC_ARTIFACT_MODE: dmg-only
|
||||||
|
RELEASE_CHANNEL: beta
|
||||||
|
RELEASE_PLATFORM: mac
|
||||||
|
RELEASE_SIGNED: "true"
|
||||||
|
RELEASE_VERSION: ${{ needs.metadata.outputs.beta_version }}
|
||||||
|
run: node --experimental-strip-types .github/scripts/release/r2/publish-platform.ts
|
||||||
|
|
||||||
|
- name: Upload mac publish manifest
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: open-design-beta-mac-release-assets
|
name: open-design-beta-mac-publish-manifest
|
||||||
path: ${{ runner.temp }}/release-assets
|
path: ${{ runner.temp }}/release-platform-manifests/mac.json
|
||||||
|
|
||||||
build_mac_intel:
|
build_mac_intel:
|
||||||
name: Build beta mac x64
|
name: Build beta mac x64
|
||||||
|
|
@ -304,8 +319,6 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v6.0.2
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v5
|
uses: pnpm/action-setup@v5
|
||||||
|
|
@ -338,16 +351,33 @@ jobs:
|
||||||
id: assets
|
id: assets
|
||||||
env:
|
env:
|
||||||
ASSET_VERSION_SUFFIX: .unsigned
|
ASSET_VERSION_SUFFIX: .unsigned
|
||||||
|
CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }}
|
||||||
RELEASE_CHANNEL: beta
|
RELEASE_CHANNEL: beta
|
||||||
RELEASE_VERSION: ${{ needs.metadata.outputs.beta_version }}
|
RELEASE_VERSION: ${{ needs.metadata.outputs.beta_version }}
|
||||||
TOOLS_PACK_NAMESPACE: release-beta-intel
|
TOOLS_PACK_NAMESPACE: release-beta-intel
|
||||||
run: bash .github/scripts/release/assets/mac-intel.sh
|
run: bash .github/scripts/release/assets/mac-intel.sh
|
||||||
|
|
||||||
- name: Upload mac intel release bundle
|
- name: Publish beta mac intel assets to R2
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_RELEASES_AK }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_RELEASES_SK }}
|
||||||
|
AWS_DEFAULT_REGION: auto
|
||||||
|
AWS_EC2_METADATA_DISABLED: "true"
|
||||||
|
CLOUDFLARE_R2_RELEASES_BUCKET: ${{ secrets.CLOUDFLARE_R2_RELEASES_BUCKET }}
|
||||||
|
CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }}
|
||||||
|
CLOUDFLARE_R2_RELEASES_URL: ${{ secrets.CLOUDFLARE_R2_RELEASES_URL }}
|
||||||
|
ASSET_VERSION_SUFFIX: ${{ needs.metadata.outputs.asset_version_suffix }}
|
||||||
|
MAC_INTEL_ASSET_SUFFIX: .unsigned
|
||||||
|
RELEASE_CHANNEL: beta
|
||||||
|
RELEASE_PLATFORM: mac-intel
|
||||||
|
RELEASE_VERSION: ${{ needs.metadata.outputs.beta_version }}
|
||||||
|
run: node --experimental-strip-types .github/scripts/release/r2/publish-platform.ts
|
||||||
|
|
||||||
|
- name: Upload mac intel publish manifest
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: open-design-beta-mac-intel-release-assets
|
name: open-design-beta-mac-intel-publish-manifest
|
||||||
path: ${{ runner.temp }}/release-assets
|
path: ${{ runner.temp }}/release-platform-manifests/macIntel.json
|
||||||
|
|
||||||
build_win:
|
build_win:
|
||||||
name: Build beta win x64
|
name: Build beta win x64
|
||||||
|
|
@ -357,8 +387,6 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v6.0.2
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v5
|
uses: pnpm/action-setup@v5
|
||||||
|
|
@ -468,6 +496,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
OD_PACKAGED_E2E_BUILD_JSON_PATH: ${{ runner.temp }}/windows-tools-pack-build.json
|
OD_PACKAGED_E2E_BUILD_JSON_PATH: ${{ runner.temp }}/windows-tools-pack-build.json
|
||||||
OD_PACKAGED_E2E_WIN: "1"
|
OD_PACKAGED_E2E_WIN: "1"
|
||||||
|
OD_PACKAGED_E2E_WIN_VERIFY_REINSTALL: "0"
|
||||||
OD_PACKAGED_E2E_NAMESPACE: release-beta-win
|
OD_PACKAGED_E2E_NAMESPACE: release-beta-win
|
||||||
OD_PACKAGED_E2E_REPORT_DIR: ${{ runner.temp }}/release-report/win
|
OD_PACKAGED_E2E_REPORT_DIR: ${{ runner.temp }}/release-report/win
|
||||||
OD_PACKAGED_E2E_TOOLS_PACK_DIR: ${{ runner.temp }}/tools-pack
|
OD_PACKAGED_E2E_TOOLS_PACK_DIR: ${{ runner.temp }}/tools-pack
|
||||||
|
|
@ -486,35 +515,6 @@ jobs:
|
||||||
path: ${{ runner.temp }}/release-report/win
|
path: ${{ runner.temp }}/release-report/win
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
|
|
||||||
- name: Prune Windows tools-pack cache
|
|
||||||
shell: pwsh
|
|
||||||
continue-on-error: true
|
|
||||||
run: ./.github/scripts/release/cache/win.ps1
|
|
||||||
|
|
||||||
- name: Save Windows tools-pack cache
|
|
||||||
if: ${{ success() && (steps.win_tools_pack_cache_restore.outputs.cache-hit != 'true' || steps.win_tools_pack_build.outputs.cache_failed == 'true') }}
|
|
||||||
uses: actions/cache/save@v5
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
path: ${{ runner.temp }}/tools-pack-cache
|
|
||||||
key: ${{ steps.win_tools_pack_cache_key.outputs.key }}
|
|
||||||
|
|
||||||
- name: Retain recent Windows tools-pack caches
|
|
||||||
if: ${{ success() }}
|
|
||||||
shell: pwsh
|
|
||||||
continue-on-error: true
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
$prefix = "${{ steps.win_tools_pack_cache_key.outputs.prefix }}"
|
|
||||||
$keep = 3
|
|
||||||
$caches = @(gh cache list --key $prefix --sort created_at --order desc --limit 100 --json id,key,createdAt | ConvertFrom-Json)
|
|
||||||
$stale = @($caches | Select-Object -Skip $keep)
|
|
||||||
foreach ($cache in $stale) {
|
|
||||||
gh cache delete $cache.id
|
|
||||||
}
|
|
||||||
"actionsCachePrefix=$prefix kept=$([Math]::Min($caches.Count, $keep)) deleted=$($stale.Count)"
|
|
||||||
|
|
||||||
- name: Prepare windows beta assets
|
- name: Prepare windows beta assets
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
env:
|
env:
|
||||||
|
|
@ -527,11 +527,27 @@ jobs:
|
||||||
WINDOWS_ASSET_SUFFIX: .unsigned
|
WINDOWS_ASSET_SUFFIX: .unsigned
|
||||||
run: ./.github/scripts/release/assets/win.ps1
|
run: ./.github/scripts/release/assets/win.ps1
|
||||||
|
|
||||||
- name: Upload windows release bundle
|
- name: Publish beta windows assets to R2
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_RELEASES_AK }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_RELEASES_SK }}
|
||||||
|
AWS_DEFAULT_REGION: auto
|
||||||
|
AWS_EC2_METADATA_DISABLED: "true"
|
||||||
|
CLOUDFLARE_R2_RELEASES_BUCKET: ${{ secrets.CLOUDFLARE_R2_RELEASES_BUCKET }}
|
||||||
|
CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }}
|
||||||
|
CLOUDFLARE_R2_RELEASES_URL: ${{ secrets.CLOUDFLARE_R2_RELEASES_URL }}
|
||||||
|
ASSET_VERSION_SUFFIX: ${{ needs.metadata.outputs.asset_version_suffix }}
|
||||||
|
RELEASE_CHANNEL: beta
|
||||||
|
RELEASE_PLATFORM: win
|
||||||
|
RELEASE_VERSION: ${{ needs.metadata.outputs.beta_version }}
|
||||||
|
WIN_ASSET_SUFFIX: .unsigned
|
||||||
|
run: node --experimental-strip-types .github/scripts/release/r2/publish-platform.ts
|
||||||
|
|
||||||
|
- name: Upload windows publish manifest
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: open-design-beta-win-release-assets
|
name: open-design-beta-win-publish-manifest
|
||||||
path: ${{ runner.temp }}/release-assets
|
path: ${{ runner.temp }}/release-platform-manifests/win.json
|
||||||
|
|
||||||
build_linux:
|
build_linux:
|
||||||
name: Build beta linux x64
|
name: Build beta linux x64
|
||||||
|
|
@ -541,8 +557,6 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v6.0.2
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v5
|
uses: pnpm/action-setup@v5
|
||||||
|
|
@ -553,6 +567,8 @@ jobs:
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: pnpm-lock.yaml
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
@ -631,14 +647,30 @@ jobs:
|
||||||
TOOLS_PACK_NAMESPACE: release-beta-linux
|
TOOLS_PACK_NAMESPACE: release-beta-linux
|
||||||
run: bash .github/scripts/release/assets/linux.sh
|
run: bash .github/scripts/release/assets/linux.sh
|
||||||
|
|
||||||
- name: Upload linux release bundle
|
- name: Publish beta linux assets to R2
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_RELEASES_AK }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_RELEASES_SK }}
|
||||||
|
AWS_DEFAULT_REGION: auto
|
||||||
|
AWS_EC2_METADATA_DISABLED: "true"
|
||||||
|
CLOUDFLARE_R2_RELEASES_BUCKET: ${{ secrets.CLOUDFLARE_R2_RELEASES_BUCKET }}
|
||||||
|
CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }}
|
||||||
|
CLOUDFLARE_R2_RELEASES_URL: ${{ secrets.CLOUDFLARE_R2_RELEASES_URL }}
|
||||||
|
ASSET_VERSION_SUFFIX: ${{ needs.metadata.outputs.asset_version_suffix }}
|
||||||
|
LINUX_ASSET_SUFFIX: .unsigned
|
||||||
|
RELEASE_CHANNEL: beta
|
||||||
|
RELEASE_PLATFORM: linux
|
||||||
|
RELEASE_VERSION: ${{ needs.metadata.outputs.beta_version }}
|
||||||
|
run: node --experimental-strip-types .github/scripts/release/r2/publish-platform.ts
|
||||||
|
|
||||||
|
- name: Upload linux publish manifest
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: open-design-beta-linux-release-assets
|
name: open-design-beta-linux-publish-manifest
|
||||||
path: ${{ runner.temp }}/release-assets
|
path: ${{ runner.temp }}/release-platform-manifests/linux.json
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Publish beta release to R2
|
name: Publish beta metadata to R2
|
||||||
needs:
|
needs:
|
||||||
- metadata
|
- metadata
|
||||||
- build_mac
|
- build_mac
|
||||||
|
|
@ -650,11 +682,7 @@ jobs:
|
||||||
always() &&
|
always() &&
|
||||||
!cancelled() &&
|
!cancelled() &&
|
||||||
needs.metadata.result == 'success' &&
|
needs.metadata.result == 'success' &&
|
||||||
(inputs.enable_mac || inputs.enable_win || inputs.enable_mac_intel || inputs.enable_linux) &&
|
(inputs.enable_mac || inputs.enable_win || inputs.enable_mac_intel || inputs.enable_linux)
|
||||||
(!inputs.enable_mac || needs.build_mac.result == 'success') &&
|
|
||||||
(!inputs.enable_mac_intel || needs.build_mac_intel.result == 'success') &&
|
|
||||||
(!inputs.enable_win || needs.build_win.result == 'success') &&
|
|
||||||
(!inputs.enable_linux || needs.build_linux.result == 'success')
|
|
||||||
}}
|
}}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
|
|
@ -674,110 +702,142 @@ jobs:
|
||||||
ENABLE_MAC: ${{ inputs.enable_mac }}
|
ENABLE_MAC: ${{ inputs.enable_mac }}
|
||||||
ENABLE_MAC_INTEL: ${{ inputs.enable_mac_intel }}
|
ENABLE_MAC_INTEL: ${{ inputs.enable_mac_intel }}
|
||||||
ENABLE_WIN: ${{ inputs.enable_win }}
|
ENABLE_WIN: ${{ inputs.enable_win }}
|
||||||
GITHUB_RELEASE_ENABLED: "false"
|
LINUX_RESULT: ${{ needs.build_linux.result }}
|
||||||
LINUX_ASSET_SUFFIX: .unsigned
|
MAC_INTEL_RESULT: ${{ needs.build_mac_intel.result }}
|
||||||
MAC_ARTIFACT_MODE: dmg-only
|
MAC_RESULT: ${{ needs.build_mac.result }}
|
||||||
MAC_INTEL_ASSET_SUFFIX: .unsigned
|
|
||||||
RELEASE_CHANNEL: beta
|
RELEASE_CHANNEL: beta
|
||||||
RELEASE_VERSION: ${{ needs.metadata.outputs.beta_version }}
|
RELEASE_VERSION: ${{ needs.metadata.outputs.beta_version }}
|
||||||
RELEASE_SIGNED: "true"
|
RELEASE_SIGNED: "true"
|
||||||
REPORT_MODE: zip
|
|
||||||
STATE_SOURCE: ${{ needs.metadata.outputs.state_source }}
|
STATE_SOURCE: ${{ needs.metadata.outputs.state_source }}
|
||||||
WIN_ASSET_SUFFIX: .unsigned
|
WIN_RESULT: ${{ needs.build_win.result }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v6.0.2
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Download mac release bundle
|
- name: Download mac publish manifest
|
||||||
if: ${{ inputs.enable_mac }}
|
if: ${{ inputs.enable_mac && needs.build_mac.result == 'success' }}
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: open-design-beta-mac-release-assets
|
name: open-design-beta-mac-publish-manifest
|
||||||
path: ${{ runner.temp }}/release-assets/mac
|
path: ${{ runner.temp }}/release-platform-manifests
|
||||||
|
|
||||||
- name: Download mac intel release bundle
|
- name: Download mac intel publish manifest
|
||||||
if: ${{ inputs.enable_mac_intel }}
|
if: ${{ inputs.enable_mac_intel && needs.build_mac_intel.result == 'success' }}
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: open-design-beta-mac-intel-release-assets
|
name: open-design-beta-mac-intel-publish-manifest
|
||||||
path: ${{ runner.temp }}/release-assets/mac-intel
|
path: ${{ runner.temp }}/release-platform-manifests
|
||||||
|
|
||||||
- name: Download windows release bundle
|
- name: Download windows publish manifest
|
||||||
if: ${{ inputs.enable_win }}
|
if: ${{ inputs.enable_win && needs.build_win.result == 'success' }}
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: open-design-beta-win-release-assets
|
name: open-design-beta-win-publish-manifest
|
||||||
path: ${{ runner.temp }}/release-assets/win
|
path: ${{ runner.temp }}/release-platform-manifests
|
||||||
|
|
||||||
- name: Download linux release bundle
|
- name: Download linux publish manifest
|
||||||
if: ${{ inputs.enable_linux }}
|
if: ${{ inputs.enable_linux && needs.build_linux.result == 'success' }}
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: open-design-beta-linux-release-assets
|
name: open-design-beta-linux-publish-manifest
|
||||||
path: ${{ runner.temp }}/release-assets/linux
|
path: ${{ runner.temp }}/release-platform-manifests
|
||||||
|
|
||||||
- name: Download linux e2e spec report
|
|
||||||
if: ${{ inputs.enable_linux }}
|
|
||||||
uses: actions/download-artifact@v8
|
|
||||||
with:
|
|
||||||
name: open-design-beta-linux-e2e-report
|
|
||||||
path: ${{ runner.temp }}/release-report/linux
|
|
||||||
|
|
||||||
- name: Download mac e2e spec report
|
|
||||||
if: ${{ inputs.enable_mac }}
|
|
||||||
uses: actions/download-artifact@v8
|
|
||||||
with:
|
|
||||||
name: open-design-beta-mac-e2e-report
|
|
||||||
path: ${{ runner.temp }}/release-report/mac
|
|
||||||
|
|
||||||
- name: Download windows e2e spec report
|
|
||||||
if: ${{ inputs.enable_win }}
|
|
||||||
uses: actions/download-artifact@v8
|
|
||||||
with:
|
|
||||||
name: open-design-beta-win-e2e-report
|
|
||||||
path: ${{ runner.temp }}/release-report/win
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
|
|
||||||
- name: Publish beta assets and metadata to R2
|
- name: Publish beta metadata to R2
|
||||||
id: r2
|
id: r2
|
||||||
run: bash .github/scripts/release/r2/publish.sh
|
run: node --experimental-strip-types .github/scripts/release/r2/publish-beta-metadata.ts
|
||||||
|
|
||||||
- name: Verify R2 beta publish
|
- name: Verify R2 beta metadata
|
||||||
env:
|
env:
|
||||||
R2_LINUX_APPIMAGE_URL: ${{ steps.r2.outputs.linux_appimage_url }}
|
R2_METADATA_URL: ${{ steps.r2.outputs.version_metadata_url }}
|
||||||
R2_MAC_DMG_URL: ${{ steps.r2.outputs.mac_dmg_url }}
|
run: node --experimental-strip-types .github/scripts/release/r2/verify-beta-metadata.ts
|
||||||
R2_MAC_FEED_URL: ${{ steps.r2.outputs.mac_feed_url }}
|
|
||||||
R2_MAC_INTEL_DMG_URL: ${{ steps.r2.outputs.mac_intel_dmg_url }}
|
|
||||||
R2_MAC_INTEL_ZIP_URL: ${{ steps.r2.outputs.mac_intel_zip_url }}
|
|
||||||
R2_MAC_ZIP_URL: ${{ steps.r2.outputs.mac_zip_url }}
|
|
||||||
R2_METADATA_URL: ${{ steps.r2.outputs.metadata_url }}
|
|
||||||
R2_REPORT_ZIP_URL: ${{ steps.r2.outputs.report_zip_url }}
|
|
||||||
R2_WIN_FEED_URL: ${{ steps.r2.outputs.win_feed_url }}
|
|
||||||
R2_WIN_INSTALLER_URL: ${{ steps.r2.outputs.win_installer_url }}
|
|
||||||
run: bash .github/scripts/release/r2/verify.sh
|
|
||||||
|
|
||||||
- name: Publish summary
|
- name: Publish summary
|
||||||
env:
|
env:
|
||||||
R2_LINUX_APPIMAGE_URL: ${{ steps.r2.outputs.linux_appimage_url }}
|
R2_METADATA_URL: ${{ steps.r2.outputs.version_metadata_url }}
|
||||||
R2_MAC_DMG_URL: ${{ steps.r2.outputs.mac_dmg_url }}
|
run: node --experimental-strip-types .github/scripts/release/r2/summary-beta.ts >> "$GITHUB_STEP_SUMMARY"
|
||||||
R2_MAC_FEED_URL: ${{ steps.r2.outputs.mac_feed_url }}
|
|
||||||
R2_MAC_INTEL_DMG_URL: ${{ steps.r2.outputs.mac_intel_dmg_url }}
|
|
||||||
R2_MAC_INTEL_ZIP_URL: ${{ steps.r2.outputs.mac_intel_zip_url }}
|
|
||||||
R2_MAC_ZIP_URL: ${{ steps.r2.outputs.mac_zip_url }}
|
|
||||||
R2_METADATA_URL: ${{ steps.r2.outputs.metadata_url }}
|
|
||||||
R2_REPORT_ZIP_URL: ${{ steps.r2.outputs.report_zip_url }}
|
|
||||||
R2_VERSION_METADATA_URL: ${{ steps.r2.outputs.version_metadata_url }}
|
|
||||||
R2_VERSION_PREFIX: ${{ steps.r2.outputs.version_prefix }}
|
|
||||||
R2_WIN_FEED_URL: ${{ steps.r2.outputs.win_feed_url }}
|
|
||||||
R2_WIN_INSTALLER_URL: ${{ steps.r2.outputs.win_installer_url }}
|
|
||||||
run: bash .github/scripts/release/r2/summary.sh
|
|
||||||
|
|
||||||
- name: Cleanup workflow artifacts
|
- name: Cleanup workflow artifacts
|
||||||
if: ${{ success() }}
|
if: ${{ success() && steps.r2.outputs.release_state == 'complete' }}
|
||||||
run: bash .github/scripts/release/github/cleanup-artifacts.sh
|
run: bash .github/scripts/release/github/cleanup-artifacts.sh
|
||||||
|
|
||||||
|
runtime_trace:
|
||||||
|
name: Runtime trace
|
||||||
|
needs:
|
||||||
|
- metadata
|
||||||
|
- build_mac
|
||||||
|
- build_mac_intel
|
||||||
|
- build_win
|
||||||
|
- build_linux
|
||||||
|
- publish
|
||||||
|
if: ${{ always() }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Summarize workflow runtime
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
RUN_ID: ${{ github.run_id }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
run_json="$RUNNER_TEMP/run.json"
|
||||||
|
gh run view "$RUN_ID" --repo "$GITHUB_REPOSITORY" --json conclusion,createdAt,databaseId,displayTitle,event,headBranch,jobs,updatedAt,url > "$run_json"
|
||||||
|
jq -r '
|
||||||
|
def parse_ts: sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601;
|
||||||
|
def seconds($start; $end):
|
||||||
|
if ($start and $end) then (($end | parse_ts) - ($start | parse_ts)) else null end;
|
||||||
|
def fmt($seconds):
|
||||||
|
if $seconds == null then "n/a"
|
||||||
|
elif $seconds >= 60 then "\(((($seconds / 60) * 10 | round) / 10))m"
|
||||||
|
else "\(($seconds | round))s"
|
||||||
|
end;
|
||||||
|
def row($cells): "| \($cells | join(" | ")) |";
|
||||||
|
|
||||||
|
.jobs as $jobs |
|
||||||
|
[
|
||||||
|
"## Runtime trace",
|
||||||
|
"",
|
||||||
|
"Run: [\(.displayTitle)](\(.url))",
|
||||||
|
"Event: `\(.event)`",
|
||||||
|
"Branch: `\(.headBranch)`",
|
||||||
|
"Elapsed: \(fmt(seconds(.createdAt; .updatedAt)))",
|
||||||
|
"",
|
||||||
|
"### Jobs",
|
||||||
|
"| Job | Result | Duration | Slowest step |",
|
||||||
|
"| --- | --- | ---: | --- |",
|
||||||
|
(
|
||||||
|
$jobs
|
||||||
|
| sort_by(seconds(.startedAt; .completedAt) // 0)
|
||||||
|
| reverse
|
||||||
|
| .[]
|
||||||
|
| select(.conclusion != "skipped")
|
||||||
|
| (
|
||||||
|
[(.steps // [])[] | select(.startedAt and .completedAt and .conclusion != "skipped") | {name, duration: seconds(.startedAt; .completedAt)}]
|
||||||
|
| max_by(.duration // 0)
|
||||||
|
) as $slow
|
||||||
|
| row([.name, (.conclusion // .status), fmt(seconds(.startedAt; .completedAt)), "\($slow.name // "n/a") (\(fmt($slow.duration)))"])
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
"### Slowest steps",
|
||||||
|
"| Step | Job | Duration |",
|
||||||
|
"| --- | --- | ---: |",
|
||||||
|
(
|
||||||
|
[
|
||||||
|
$jobs[] as $job
|
||||||
|
| ($job.steps // [])[]
|
||||||
|
| select(.startedAt and .completedAt and .conclusion != "skipped")
|
||||||
|
| {job: $job.name, name, duration: seconds(.startedAt; .completedAt)}
|
||||||
|
]
|
||||||
|
| sort_by(.duration // 0)
|
||||||
|
| reverse
|
||||||
|
| .[0:20][]
|
||||||
|
| row([.name, .job, fmt(.duration)])
|
||||||
|
)
|
||||||
|
][]
|
||||||
|
' "$run_json" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ const toolsPackDir = resolveFromWorkspace(process.env.OD_PACKAGED_E2E_TOOLS_PACK
|
||||||
const namespace = process.env.OD_PACKAGED_E2E_NAMESPACE ?? 'release-beta-win';
|
const namespace = process.env.OD_PACKAGED_E2E_NAMESPACE ?? 'release-beta-win';
|
||||||
const toolsPackBin = join(workspaceRoot, 'tools', 'pack', 'bin', 'tools-pack.mjs');
|
const toolsPackBin = join(workspaceRoot, 'tools', 'pack', 'bin', 'tools-pack.mjs');
|
||||||
const maxInstallDurationMs = Number.parseInt(process.env.OD_PACKAGED_E2E_WIN_MAX_INSTALL_MS ?? '120000', 10);
|
const maxInstallDurationMs = Number.parseInt(process.env.OD_PACKAGED_E2E_WIN_MAX_INSTALL_MS ?? '120000', 10);
|
||||||
|
const verifyReinstallWhileRunning = process.env.OD_PACKAGED_E2E_WIN_VERIFY_REINSTALL !== '0';
|
||||||
const installIdentity = resolveInstallIdentity(namespace);
|
const installIdentity = resolveInstallIdentity(namespace);
|
||||||
|
|
||||||
const outputNamespaceRoot = join(toolsPackDir, 'out', 'win', 'namespaces', namespace);
|
const outputNamespaceRoot = join(toolsPackDir, 'out', 'win', 'namespaces', namespace);
|
||||||
|
|
@ -196,26 +197,29 @@ winDescribe('packaged windows runtime smoke', () => {
|
||||||
expect(value.health.ok).toBe(true);
|
expect(value.health.ok).toBe(true);
|
||||||
expect(value.health.version).toEqual(expect.any(String));
|
expect(value.health.version).toEqual(expect.any(String));
|
||||||
|
|
||||||
const reinstall = await measureSmokeStep(timings, 'direct reinstall while running', async () =>
|
let reinstall: DirectInstallerResult | { skipped: true } = { skipped: true };
|
||||||
runDirectInstaller(install.installerPath, install.installDir),
|
if (verifyReinstallWhileRunning) {
|
||||||
);
|
reinstall = await measureSmokeStep(timings, 'direct reinstall while running', async () =>
|
||||||
started = false;
|
runDirectInstaller(install.installerPath, install.installDir),
|
||||||
expect(reinstall.code).toBe(0);
|
);
|
||||||
expect(reinstall.nsisLogTail.join('\n')).toContain('running instances detected before silent install');
|
started = false;
|
||||||
expect(reinstall.nsisLogTail.join('\n')).toContain('running instances close exit=0');
|
expect(reinstall.code).toBe(0);
|
||||||
|
expect(reinstall.nsisLogTail.join('\n')).toContain('running instances detected before silent install');
|
||||||
|
expect(reinstall.nsisLogTail.join('\n')).toContain('running instances close exit=0');
|
||||||
|
|
||||||
start = await measureSmokeStep(timings, 'restart after direct reinstall', async () =>
|
start = await measureSmokeStep(timings, 'restart after direct reinstall', async () =>
|
||||||
runToolsPackJson<WinStartResult>('start'),
|
runToolsPackJson<WinStartResult>('start'),
|
||||||
);
|
);
|
||||||
started = true;
|
started = true;
|
||||||
expect(start.namespace).toBe(namespace);
|
expect(start.namespace).toBe(namespace);
|
||||||
expect(start.source).toBe('installed');
|
expect(start.source).toBe('installed');
|
||||||
expectPathInside(start.executablePath, install.installDir);
|
expectPathInside(start.executablePath, install.installDir);
|
||||||
|
|
||||||
const postReinstallInspect = await measureSmokeStep(timings, 'wait healthy inspect after reinstall', async () =>
|
const postReinstallInspect = await measureSmokeStep(timings, 'wait healthy inspect after reinstall', async () =>
|
||||||
waitForHealthyDesktop(),
|
waitForHealthyDesktop(),
|
||||||
);
|
);
|
||||||
expect(postReinstallInspect.status?.state).toBe('running');
|
expect(postReinstallInspect.status?.state).toBe('running');
|
||||||
|
}
|
||||||
|
|
||||||
await mkdir(dirname(screenshotPath), { recursive: true });
|
await mkdir(dirname(screenshotPath), { recursive: true });
|
||||||
const screenshot = await measureSmokeStep(timings, 'inspect screenshot', async () =>
|
const screenshot = await measureSmokeStep(timings, 'inspect screenshot', async () =>
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ describe("packaged smoke workflow", () => {
|
||||||
expect(workflow).not.toContain("actions/cache/save");
|
expect(workflow).not.toContain("actions/cache/save");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves beta linux AppImage smoke reports for release publication", async () => {
|
it("preserves beta linux AppImage smoke reports for platform publication", async () => {
|
||||||
const workflow = await readFile(releaseBetaWorkflowPath, "utf8");
|
const workflow = await readFile(releaseBetaWorkflowPath, "utf8");
|
||||||
const linuxBuildStep = workflow.match(
|
const linuxBuildStep = workflow.match(
|
||||||
/- name: Build beta linux artifacts\n(?:.+\n)+?(?=\n - name: Smoke beta linux AppImage runtime)/m,
|
/- name: Build beta linux artifacts\n(?:.+\n)+?(?=\n - name: Smoke beta linux AppImage runtime)/m,
|
||||||
|
|
@ -38,7 +38,11 @@ describe("packaged smoke workflow", () => {
|
||||||
expect(workflow).toContain("tools-pack.json");
|
expect(workflow).toContain("tools-pack.json");
|
||||||
expect(workflow).toContain("Upload linux e2e spec report");
|
expect(workflow).toContain("Upload linux e2e spec report");
|
||||||
expect(workflow).toContain("open-design-beta-linux-e2e-report");
|
expect(workflow).toContain("open-design-beta-linux-e2e-report");
|
||||||
expect(workflow).toContain("Download linux e2e spec report");
|
expect(workflow).toContain("Publish beta linux assets to R2");
|
||||||
|
expect(workflow).toContain("RELEASE_PLATFORM: linux");
|
||||||
|
expect(workflow).toContain("Upload linux publish manifest");
|
||||||
|
expect(workflow).toContain("open-design-beta-linux-publish-manifest");
|
||||||
|
expect(workflow).not.toContain("Download linux e2e spec report");
|
||||||
expectReleaseLinuxBuildPreservesEvidence(workflow, "Build beta linux artifacts");
|
expectReleaseLinuxBuildPreservesEvidence(workflow, "Build beta linux artifacts");
|
||||||
expectReleaseLinuxSmokePreservesEvidenceBeforeApt(workflow, "Smoke beta linux AppImage runtime");
|
expectReleaseLinuxSmokePreservesEvidenceBeforeApt(workflow, "Smoke beta linux AppImage runtime");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -229,8 +229,9 @@ at the top of each file and re-run. Bump the hash whenever
|
||||||
|
|
||||||
## CI
|
## CI
|
||||||
|
|
||||||
`.github/workflows/nix-check.yml` runs `nix flake check` followed by
|
`.github/workflows/nix-check.yml` runs `nix flake check` on pushes to
|
||||||
separate `nix build .#daemon` and `nix build .#web` steps on each push
|
`main` and can also be started manually with `workflow_dispatch`. It is
|
||||||
that touches the flake or the lockfile. Build artifacts are cached on
|
not a default pull request gate: the flake is a community installation
|
||||||
the `nexu-open-design` Cachix instance — PRs from forks read from the
|
and deployment surface, while regular PR validation stays focused on the
|
||||||
cache without needing the auth token.
|
primary product delivery checks. The flake check already builds the
|
||||||
|
`daemon` and `web` checks declared in `flake.nix`.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue