Skip to content

Instantly share code, notes, and snippets.

@kolibril13
Created March 25, 2026 11:57
Show Gist options
  • Select an option

  • Save kolibril13/dc89eb65dea968a15f0c67972fa17d42 to your computer and use it in GitHub Desktop.

Select an option

Save kolibril13/dc89eb65dea968a15f0c67972fa17d42 to your computer and use it in GitHub Desktop.
Generates a Blender extensions repository index.json by fetching manifests and release assets from configured GitHub repositories.
// Generates the static Blender extensions repository JSON during the build by
// fetching the latest release assets and manifests from the configured GitHub repos.
// This is a narrow JavaScript rewrite of Blender's Python
// `subcmd_server.generate()` implementation, which powers the
// `blender --command extension server-generate` command in:
// https://raw.githubusercontent.com/blender/blender/refs/tags/v5.1.0/scripts/addons_core/bl_pkg/cli/blender_ext.py
//
// Unlike Blender's full implementation, this script does not scan local zip files
// or generate HTML. It only reproduces the repository JSON generation step for GitHub Releases based workflow.
import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parse as parseToml } from '@iarna/toml';
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const generatedRepositoryPath = resolve(rootDir, 'public/blender-extensions/index.json');
const generatedRepositoryDir = dirname(generatedRepositoryPath);
const githubToken = process.env.GITHUB_TOKEN?.trim();
const extensionSources = [
{
owner: 'kolibril13',
repo: 'blender_best_presets',
manifestPath: 'blender_manifest.toml'
},
{
owner: 'kolibril13',
repo: 'blender_csv_import',
manifestPath: 'csv_importer/blender_manifest.toml'
}
];
const githubRequestHeaders = {
Accept: 'application/vnd.github+json',
'User-Agent': 'jan-hendrik-mueller.de/blender-extensions-sync',
...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {})
};
function cleanObjectEntries(value) {
return Object.fromEntries(
Object.entries(value).filter(([, entryValue]) => entryValue !== null && entryValue !== undefined)
);
}
async function fetchJson(url) {
const response = await fetch(url, { headers: githubRequestHeaders });
if (!response.ok) {
const responseText = (await response.text()).trim();
const responseSummary = responseText ? ` Response: ${responseText}` : '';
const authGuidance =
(response.status === 401 || response.status === 403) && !githubToken
? ' Set GITHUB_TOKEN in your local environment or Cloudflare Pages Variables and Secrets.'
: '';
throw new Error(
`GitHub API request failed (${response.status}) for ${url}.${authGuidance}${responseSummary}`
);
}
return response.json();
}
async function fetchText(url) {
const response = await fetch(url, { headers: githubRequestHeaders });
if (!response.ok) {
throw new Error(`Request failed (${response.status}) for ${url}`);
}
return response.text();
}
function normalizeManifest(manifest) {
const buildGenerated = manifest.build?.generated;
const normalizedManifest = { ...manifest };
if (buildGenerated?.platforms) {
normalizedManifest.platforms = buildGenerated.platforms;
}
if (buildGenerated?.wheels) {
normalizedManifest.wheels = buildGenerated.wheels;
}
return normalizedManifest;
}
function inferPlatformsFromAssetName(assetName) {
const normalizedName = assetName.toLowerCase();
const inferredPlatforms = [];
if (normalizedName.includes('windows_x64')) {
inferredPlatforms.push('windows-x64');
}
if (normalizedName.includes('linux_x64')) {
inferredPlatforms.push('linux-x64');
}
if (normalizedName.includes('macos_arm64')) {
inferredPlatforms.push('macos-arm64');
}
if (normalizedName.includes('macos_x64')) {
inferredPlatforms.push('macos-x64');
}
return inferredPlatforms;
}
function buildBaseRepositoryEntry(manifest) {
const { build, wheels, ...manifestData } = normalizeManifest(manifest);
return cleanObjectEntries(manifestData);
}
function buildRepositoryEntry(baseEntry, asset) {
const inferredPlatforms = inferPlatformsFromAssetName(asset.name);
return {
...baseEntry,
...(inferredPlatforms.length > 0 ? { platforms: inferredPlatforms } : {}),
archive_url: asset.browser_download_url,
archive_size: asset.size,
archive_hash: asset.digest ?? ''
};
}
async function loadRepositoryEntriesForSource(source) {
const release = await fetchJson(
`https://api.github.com/repos/${source.owner}/${source.repo}/releases/latest`
);
const manifestText = await fetchText(
`https://raw.githubusercontent.com/${source.owner}/${source.repo}/${release.tag_name}/${source.manifestPath}`
);
const manifest = parseToml(manifestText);
const baseEntry = buildBaseRepositoryEntry(manifest);
const zipAssets = release.assets
.filter((asset) => asset.name.endsWith('.zip'))
.sort((left, right) => left.name.localeCompare(right.name));
if (zipAssets.length === 0) {
throw new Error(`No zip assets found for ${source.owner}/${source.repo}`);
}
return zipAssets.map((asset) => {
console.log(`Using ${asset.name} from ${source.owner}/${source.repo}`);
return buildRepositoryEntry(baseEntry, asset);
});
}
async function main() {
if (!githubToken) {
console.warn(
'GITHUB_TOKEN is not set. GitHub API requests may fail in CI or Cloudflare Pages due to anonymous rate limits.'
);
}
const repositoryData = (await Promise.all(extensionSources.map(loadRepositoryEntriesForSource)))
.flat()
.sort((left, right) =>
left.name.localeCompare(right.name) || left.archive_url.localeCompare(right.archive_url)
);
const generatedRepository = {
version: 'v1',
blocklist: [],
data: repositoryData
};
mkdirSync(generatedRepositoryDir, { recursive: true });
writeFileSync(generatedRepositoryPath, `${JSON.stringify(generatedRepository, null, 2)}\n`);
console.log(`Wrote ${generatedRepositoryPath}`);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment