Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions .github/workflows/release-macos-aarch64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ jobs:
tauriScript: pnpm exec tauri -vvv
args: ${{ matrix.args }}
retryAttempts: 3
uploadUpdaterJson: true
uploadUpdaterJson: false
updaterJsonPreferNsis: true
releaseAssetNamePattern: openwork-desktop-[platform]-[arch][ext]

Expand Down Expand Up @@ -553,7 +553,7 @@ jobs:
tauriScript: pnpm exec tauri -vvv
args: ${{ matrix.args }}
retryAttempts: 3
uploadUpdaterJson: true
uploadUpdaterJson: false
updaterJsonPreferNsis: true
releaseAssetNamePattern: openwork-desktop-[platform]-[arch][ext]

Expand Down Expand Up @@ -600,6 +600,39 @@ jobs:

echo "Found bundled versions.json at $manifest_path"

publish-updater-json:
name: Publish consolidated latest.json
needs: [resolve-release, verify-release, publish-tauri]
if: needs.resolve-release.outputs.build_tauri == 'true'
runs-on: ubuntu-latest
env:
RELEASE_TAG: ${{ needs.resolve-release.outputs.release_tag }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.RELEASE_TAG }}
fetch-depth: 0

- name: Generate latest.json from release assets
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
node scripts/release/generate-latest-json.mjs \
--tag "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--output "$RUNNER_TEMP/latest.json"

- name: Upload latest.json
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh release upload "$RELEASE_TAG" "$RUNNER_TEMP/latest.json#latest.json" \
--repo "$GITHUB_REPOSITORY" \
--clobber

release-orchestrator-sidecars:
name: Build + Upload openwork-orchestrator Sidecars
needs: [resolve-release, verify-release]
Expand Down
200 changes: 200 additions & 0 deletions scripts/release/generate-latest-json.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
#!/usr/bin/env node

const ARCH_ALIASES = new Map([
["x64", "x86_64"],
["amd64", "x86_64"],
["arm64", "aarch64"],
]);

function normalizeArch(arch) {
const key = String(arch || "").trim().toLowerCase();
return ARCH_ALIASES.get(key) || key;
}

function parseArgs(argv) {
const options = {
tag: process.env.RELEASE_TAG || "",
repo: process.env.GITHUB_REPOSITORY || "different-ai/openwork",
output: "latest.json",
};

for (let i = 2; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === "--tag") {
options.tag = argv[i + 1] || "";
i += 1;
continue;
}
if (arg === "--repo") {
options.repo = argv[i + 1] || options.repo;
i += 1;
continue;
}
if (arg === "--output") {
options.output = argv[i + 1] || options.output;
i += 1;
continue;
}
throw new Error(`Unknown argument: ${arg}`);
}

if (!options.tag) {
throw new Error("Missing release tag. Pass --tag vX.Y.Z or set RELEASE_TAG.");
}

return options;
}

function updaterPlatformKeys(assetName) {
if (!assetName.startsWith("openwork-desktop-")) return [];

const stem = assetName.slice("openwork-desktop-".length);

if (stem.endsWith(".app.tar.gz")) {
const match = stem.match(/^([^-]+)-([^.]+)\.app\.tar\.gz$/);
if (!match) return [];
const platform = match[1];
const arch = normalizeArch(match[2]);
const base = `${platform}-${arch}`;
if (platform === "darwin") {
return [base, `${base}-app`];
}
return [base];
}

if (stem.endsWith(".msi")) {
const match = stem.match(/^([^-]+)-([^.]+)\.msi$/);
if (!match) return [];
const platform = match[1];
const arch = normalizeArch(match[2]);
const base = `${platform}-${arch}`;
return [base, `${base}-msi`];
}

if (stem.endsWith(".deb")) {
const match = stem.match(/^([^-]+)-([^.]+)\.deb$/);
if (!match) return [];
const platform = match[1];
const arch = normalizeArch(match[2]);
const base = `${platform}-${arch}`;
return [base, `${base}-deb`];
}

if (stem.endsWith(".rpm")) {
const match = stem.match(/^([^-]+)-([^.]+)\.rpm$/);
if (!match) return [];
const platform = match[1];
const arch = normalizeArch(match[2]);
return [`${platform}-${arch}-rpm`];
}

if (stem.endsWith(".AppImage")) {
const match = stem.match(/^([^-]+)-([^.]+)\.AppImage$/);
if (!match) return [];
const platform = match[1];
const arch = normalizeArch(match[2]);
return [`${platform}-${arch}`];
}

return [];
}

function authHeaders() {
const headers = {
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "openwork-release-latest-json",
};
const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
if (token) {
headers.Authorization = `Bearer ${token}`;
}
return headers;
}

async function fetchJson(url) {
const response = await fetch(url, { headers: authHeaders() });
if (!response.ok) {
throw new Error(`GitHub API request failed (${response.status}): ${url}`);
}
return response.json();
}

async function fetchText(url) {
const response = await fetch(url, { headers: authHeaders() });
if (!response.ok) {
throw new Error(`Failed to download signature (${response.status}): ${url}`);
}
return response.text();
}

function sortObjectEntries(input) {
const sorted = {};
for (const key of Object.keys(input).sort()) {
sorted[key] = input[key];
}
return sorted;
}

async function main() {
const { tag, repo, output } = parseArgs(process.argv);
const releaseUrl = `https://api.github.com/repos/${repo}/releases/tags/${encodeURIComponent(tag)}`;
const release = await fetchJson(releaseUrl);

const assets = Array.isArray(release.assets) ? release.assets : [];
const assetsByName = new Map();
for (const asset of assets) {
if (asset && typeof asset.name === "string") {
assetsByName.set(asset.name, asset);
}
}

const platforms = {};

for (const asset of assets) {
if (!asset || typeof asset.name !== "string" || !asset.name.endsWith(".sig")) continue;

const targetName = asset.name.slice(0, -4);
const targetAsset = assetsByName.get(targetName);
if (!targetAsset || typeof targetAsset.browser_download_url !== "string") continue;

const keys = updaterPlatformKeys(targetName);
if (!keys.length) continue;

const signature = (await fetchText(asset.browser_download_url)).trim();
if (!signature) continue;

for (const key of keys) {
platforms[key] = {
signature,
url: targetAsset.browser_download_url,
};
}
}

if (!Object.keys(platforms).length) {
throw new Error(`No updater platforms were resolved for ${repo}@${tag}.`);
}

const version = String(release.tag_name || tag).replace(/^v/, "");
const latest = {
version,
notes:
typeof release.body === "string" && release.body.trim()
? release.body
: "See the assets to download this version and install.",
pub_date: release.published_at || new Date().toISOString(),
platforms: sortObjectEntries(platforms),
};

const fs = await import("node:fs/promises");
await fs.writeFile(output, `${JSON.stringify(latest, null, 2)}\n`, "utf8");

console.log(`Wrote ${output} with ${Object.keys(latest.platforms).length} updater platforms.`);
}

main().catch((error) => {
const message = error instanceof Error ? error.stack || error.message : String(error);
console.error(message);
process.exit(1);
});
Loading