wip: improve update-specs script

This commit is contained in:
2026-01-11 09:07:34 +00:00
parent 1eab53b7ba
commit 164729b57e
27 changed files with 998 additions and 857 deletions

View File

@@ -1,39 +1,85 @@
/**
* Fetches spec documents and diagrams from the common-flow GitHub repo
* and writes them to the appropriate locations for Astro to consume.
*
* Versions are discovered from git tags and filtered based on config.
*/
import * as fs from "node:fs";
import * as path from "node:path";
import * as semver from "semver";
import { $ } from "bun";
import { config } from "../src/config";
const config = {
currentVersion: "1.0.0-rc.5",
versions: [
"1.0.0-rc.5",
"1.0.0-rc.4",
"1.0.0-rc.3",
"1.0.0-rc.2",
"1.0.0-rc.1",
],
update: {
urlTemplate:
"https://github.com/jimeh/common-flow/raw/{{version}}/{{file}}",
bodyTemplate: `---
const updateConfig = {
bodyTemplate: `---
title: {{title}}
version: {{version}}
---
{{content}}`,
imgTemplate:
'<img src="/spec/{{file}}" alt="{{title}} diagram" width="100%" />',
outputDir: "src/content/spec",
publicDir: "public/spec",
files: {
document: "common-flow.md",
diagram: "common-flow.svg",
},
},
outputDir: "src/content/spec",
publicDir: "public/spec",
};
/**
* Fetch all tags from the GitHub repository.
*/
async function fetchTags(repository: string): Promise<string[]> {
const repoUrl = `https://github.com/${repository}.git`;
console.log(`Fetching tags from ${repoUrl}...`);
try {
const result = await $`git ls-remote --tags ${repoUrl}`.text();
return result
.split("\n")
.filter(Boolean)
.map((line: string) => line.match(/refs\/tags\/(.+)$/)?.[1])
.filter(
(tag: string | undefined): tag is string =>
tag !== undefined && !tag.endsWith("^{}"),
);
} catch (error) {
throw new Error(
`Failed to fetch tags: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* Get the prerelease type of a version (e.g., "rc", "draft", or null for
* stable).
*/
function getPrereleaseType(version: string): string | null {
const prerelease = semver.prerelease(version);
if (!prerelease) return null;
return String(prerelease[0]);
}
/**
* Filter tags based on discovery configuration.
* Stable versions are always included; prereleases only if their type is in
* the includePrereleaseTypes list.
*/
function filterVersions(tags: string[]): string[] {
const { includePrereleaseTypes, excludeVersions } = config.update.discovery;
return tags.filter((tag) => {
// Must be valid semver
if (!semver.valid(tag)) return false;
// Check explicit exclusions
if (excludeVersions.includes(tag)) return false;
// Stable versions are always included
const prereleaseType = getPrereleaseType(tag);
if (prereleaseType === null) return true;
// Prereleases only if their type is in the list
return includePrereleaseTypes.includes(prereleaseType);
});
}
function buildFileUrl(
fileType: "document" | "diagram",
version: string,
@@ -65,19 +111,36 @@ function writeFile(filePath: string, content: string, comment = ""): void {
console.log(` - ${filePath}${comment}`);
}
function removeAllSpecs(): void {
console.log("\nRemoving existing spec files:");
/**
* Remove spec files for versions not in the provided list.
* Files for versions in the list are left alone (they'll be overwritten).
*/
function removeStaleSpecs(versionsToKeep: string[]): void {
const keepSet = new Set(versionsToKeep);
let removedAny = false;
for (const dir of [config.update.outputDir, config.update.publicDir]) {
if (fs.existsSync(dir)) {
const files = fs.readdirSync(dir);
for (const file of files) {
for (const dir of [updateConfig.outputDir, updateConfig.publicDir]) {
if (!fs.existsSync(dir)) continue;
const files = fs.readdirSync(dir);
for (const file of files) {
// Extract version from filename (e.g., "1.0.0-rc.1.md" -> "1.0.0-rc.1")
const version = path.basename(file, path.extname(file));
if (!keepSet.has(version)) {
if (!removedAny) {
console.log("\nRemoving stale spec files:");
removedAny = true;
}
const filePath = path.join(dir, file);
fs.unlinkSync(filePath);
console.log(` ${filePath}`);
}
}
}
if (!removedAny) {
console.log("\nNo stale spec files to remove.");
}
}
interface Spec {
@@ -104,16 +167,8 @@ async function fetchSpec(version: string): Promise<Spec> {
// Extract title from first line (after version replacement)
const title = document.split("\n", 1)[0];
// If diagram exists, inject image tag after the title
if (diagram) {
const imgTag = config.update.imgTemplate
.replace("{{file}}", `${version}.svg`)
.replace("{{title}}", title);
document = document.replace(/^(.*\n=+\n)/, `$1\n${imgTag}\n`);
}
// Build body with frontmatter
const body = config.update.bodyTemplate
const body = updateConfig.bodyTemplate
.replace("{{content}}", document)
.replace("{{title}}", title)
.replace("{{version}}", version);
@@ -127,21 +182,38 @@ async function fetchSpec(version: string): Promise<Spec> {
}
async function main(): Promise<void> {
removeAllSpecs();
// 1. Discover and filter versions
const tags = await fetchTags(config.update.repository);
console.log(`Found ${tags.length} tags`);
console.log("\nFetching configured spec versions:");
const filtered = filterVersions(tags);
const sorted = semver.rsort([...filtered]);
for (const version of config.versions) {
console.log(`\nIncluded ${sorted.length} versions after filtering:`);
console.log(` ${sorted.join(", ")}`);
if (sorted.length === 0) {
console.error("\nNo versions to process. Exiting.");
process.exit(1);
}
// 2. Remove spec files for versions no longer in the list
removeStaleSpecs(sorted);
// 3. Fetch specs for all versions
console.log("\nFetching spec documents:");
for (const version of sorted) {
try {
const spec = await fetchSpec(version);
// Write markdown file to content collection
const mdPath = path.join(config.update.outputDir, `${version}.md`);
const mdPath = path.join(updateConfig.outputDir, `${version}.md`);
writeFile(mdPath, spec.body);
// Write SVG diagram to public directory
if (spec.diagram) {
const svgPath = path.join(config.update.publicDir, `${version}.svg`);
const svgPath = path.join(updateConfig.publicDir, `${version}.svg`);
writeFile(svgPath, spec.diagram);
}
} catch (error) {