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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Versions follow [Semantic Versioning](https://semver.org/).
## [Unreleased]

### Added
- **Auto-parsed /releases page**: `site/lib/parseChangelog.ts` now extracts release metadata from CHANGELOG.md at build time. The releases page dynamically renders all versions, sections, and summaries - no manual duplication required. Sitemap includes /releases with 0.8 priority.
- **NuGet downloads badge**: Added to README linking to the GauntletCI NuGet package page.
- **Self-contained binary releases**: `release.yml` now publishes self-contained single-file executables for win-x64, win-arm64, osx-x64, osx-arm64, linux-x64, and linux-arm64, plus a `checksums.txt`, as GitHub release assets alongside the NuGet package.
- **Homebrew tap**: Created `EricCogen/homebrew-gauntletci` tap. Install with `brew tap EricCogen/gauntletci && brew install gauntletci`. SHA256 values auto-updated after each release via `update-homebrew-tap.yml`.
Expand Down
2 changes: 2 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ static export). Work in this phase:
- Pagefind full-text search (client-side, zero backend, build-time indexed)
- Playwright e2e test suite including a link-graph test that enforces every page
has at least one inbound and one outbound content link
- Releases page (`/releases`) auto-parses CHANGELOG.md at build time, eliminating
manual duplication of version metadata

---

Expand Down
177 changes: 2 additions & 175 deletions site/app/releases/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from "next";
import Link from "next/link";
import { Header } from "@/components/header";
import { Footer } from "@/components/footer";
import { parseChangelog } from "@/lib/parseChangelog";

export const metadata: Metadata = {
title: "Releases | GauntletCI",
Expand All @@ -10,181 +11,7 @@ export const metadata: Metadata = {
alternates: { canonical: "/releases" },
};

type Entry = { text: string; link?: string };
type Section = { label: "Added" | "Changed" | "Fixed"; entries: Entry[] };
type Release = {
version: string;
date: string;
tag: string;
summary: string;
sections: Section[];
compareUrl?: string;
};

const releases: Release[] = [
{
version: "2.1.1",
date: "2026-04-28",
tag: "latest",
summary: "NuGet README improvements and packaging cleanup.",
compareUrl: "https://github.com/EricCogen/GauntletCI/compare/v2.0.4...HEAD",
sections: [
{
label: "Changed",
entries: [
{
text: "NuGet package now uses a dedicated nuget-readme.md with plain markdown and absolute image URLs for correct rendering on NuGet.org.",
},
{
text: "Resized NuGet logo (200x262) added as GauntletCI-nuget.png.",
},
{
text: "30 precision-hardened detection rules: per-rule corpus validation against 618 real .NET OSS pull requests. Rules GCI0003-GCI0047 updated with tighter guards and labeler alignment.",
},
{
text: "action.yml marketplace readiness: input defaults corrected, gauntletci-version bumped to 2.1.0.",
},
],
},
{
label: "Added",
entries: [
{
text: "GitLab CI and Bitbucket Pipelines integration snippets in the docs.",
link: "/docs/integrations",
},
{
text: "Per-rule detail pages at /docs/rules with real-world case study links.",
link: "/docs/rules",
},
],
},
],
},
{
version: "2.0.4",
date: "2026-04-25",
tag: "",
summary: "Site infrastructure: search, rule detail pages, E2E tests, and author attribution.",
compareUrl: "https://github.com/EricCogen/GauntletCI/compare/v2.0.3...v2.0.4",
sections: [
{
label: "Added",
entries: [
{ text: "Full-text search with Cmd/Ctrl+K shortcut across all 53 pages." },
{ text: "30 per-rule detail pages at /docs/rules/[ruleId].", link: "/docs/rules" },
{ text: "/about page with founder bio and E-E-A-T author attribution.", link: "/about" },
{ text: "Playwright E2E test suite with smoke, article, rule detail, and link-graph tests." },
{ text: "JSON-LD schemas on all docs and rule detail pages." },
],
},
{
label: "Changed",
entries: [
{ text: "Header nav simplified to 3 items: Product dropdown, Docs, About." },
],
},
{
label: "Fixed",
entries: [
{ text: "/docs pages had zero outbound content links - link-graph test now catches this." },
{ text: "/pricing had no inbound content links from any other page." },
],
},
],
},
{
version: "2.0.3",
date: "2026-04-24",
tag: "",
summary: "Rich PR review summaries with Why, Action, and Evidence sections.",
compareUrl: "https://github.com/EricCogen/GauntletCI/compare/v2.0.2...v2.0.3",
sections: [
{
label: "Added",
entries: [
{
text: "GitHub PR review comments now include Why, Action, and Evidence sections in collapsible details blocks.",
},
{
text: "--with-llm enrichment attaches plain-English explanations to high-confidence findings.",
},
],
},
],
},
{
version: "2.0.2",
date: "2026-04-24",
tag: "",
summary: "Finding grouping and structured GitHub Actions output.",
compareUrl: "https://github.com/EricCogen/GauntletCI/compare/v2.0.1...v2.0.2",
sections: [
{
label: "Added",
entries: [
{
text: "Duplicate findings across multiple files are collapsed into a single annotated entry.",
},
{
text: "Structured Markdown output for GitHub Actions annotations and Checks summaries.",
},
],
},
],
},
{
version: "2.0.1",
date: "2026-04-24",
tag: "",
summary: "Demo links and footer fixes.",
compareUrl: "https://github.com/EricCogen/GauntletCI/compare/v2.0.0...v2.0.1",
sections: [
{
label: "Added",
entries: [
{
text: "GauntletCI-Demo links in header, footer, and README.",
link: "https://github.com/EricCogen/GauntletCI-Demo",
},
{ text: "Footer added to all /docs pages." },
],
},
{
label: "Fixed",
entries: [{ text: "Footer anchor links now use / prefix for correct cross-page navigation." }],
},
],
},
{
version: "2.0.0",
date: "2026-04-14",
tag: "initial",
summary: "Initial public release. 30 detection rules, local LLM support, MCP server, GitHub Actions integration.",
compareUrl: "https://github.com/EricCogen/GauntletCI/releases/tag/v2.0.0",
sections: [
{
label: "Added",
entries: [
{ text: "30 built-in deterministic detection rules (GCI0001-GCI0050)." },
{ text: "Local LLM enrichment via Ollama - no data leaves your machine." },
{ text: "MCP server for AI assistant integration." },
{ text: "GitHub Actions integration with inline PR comments and Checks annotations." },
{ text: "Baseline mode: suppress pre-existing findings, surface only new risks." },
{ text: "NuGet packaging and publish workflow." },
],
},
{
label: "Fixed",
entries: [
{
text: "Culture-invariant percent formatting in MarkdownReportExporter (non-en-US locales).",
},
],
},
],
},
];
const releases = parseChangelog();

const sectionColors: Record<string, string> = {
Added: "text-emerald-400 border-emerald-500/30 bg-emerald-500/10",
Expand Down
1 change: 1 addition & 0 deletions site/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
{ url: `${BASE_URL}/docs/custom-rules`, changeFrequency: "monthly", priority: 0.7 },
{ url: `${BASE_URL}/detections`, changeFrequency: "monthly", priority: 0.9 },
{ url: `${BASE_URL}/pricing`, changeFrequency: "monthly", priority: 0.8 },
{ url: `${BASE_URL}/releases`, changeFrequency: "monthly", priority: 0.8 },
{ url: `${BASE_URL}/about`, changeFrequency: "monthly", priority: 0.7 },
{ url: `${BASE_URL}/why-tests-miss-bugs`, changeFrequency: "monthly", priority: 0.8 },
{ url: `${BASE_URL}/why-code-review-misses-bugs`, changeFrequency: "monthly", priority: 0.8 },
Expand Down
159 changes: 159 additions & 0 deletions site/lib/parseChangelog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import fs from "fs";
import path from "path";

export type Section = "Added" | "Changed" | "Fixed" | "Deprecated" | "Removed" | "Security";

export interface ChangeEntry {
text: string;
link?: string;
}

export interface ReleaseSection {
label: Section;
entries: ChangeEntry[];
}

export interface Release {
version: string;
date: string;
tag: string;
summary: string;
sections: ReleaseSection[];
compareUrl?: string;
}

const VALID_SECTIONS: Set<string> = new Set(["Added", "Changed", "Fixed", "Deprecated", "Removed", "Security"]);

export function parseChangelog(): Release[] {
// Try multiple paths to find CHANGELOG.md
const possiblePaths = [
path.join(process.cwd(), "..", "..", "CHANGELOG.md"),
path.join(process.cwd(), "../../CHANGELOG.md"),
path.join(process.cwd(), "../CHANGELOG.md"),
path.join(process.cwd(), "CHANGELOG.md"),
];

let content: string | null = null;
let foundPath = "";

for (const filePath of possiblePaths) {
try {
if (fs.existsSync(filePath)) {
content = fs.readFileSync(filePath, "utf-8");
foundPath = filePath;
break;
}
} catch {
// Continue to next path
}
}

if (!content) {
console.warn("CHANGELOG.md not found at any expected path");
return [];
}

const releases: Release[] = [];
const lines = content.split("\n");

let currentVersion = "";
let currentDate = "";
let currentSectionLabel: Section | null = null;
let currentSectionEntries: ChangeEntry[] = [];
let allSections: ReleaseSection[] = [];

for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();

// Match version header: ## [1.2.3] - YYYY-MM-DD or ## [Unreleased]
const versionMatch = trimmed.match(/^##\s+\[([^\]]+)\](?:\s+-\s+(\d{4}-\d{2}-\d{2}))?$/);
if (versionMatch) {
// Save previous release if any
if (currentVersion && allSections.length > 0) {
releases.push({
version: currentVersion,
date: currentDate,
tag: currentVersion === "Unreleased" ? "unreleased" : releases.length === 0 ? "latest" : "",
summary: generateSummary(allSections),
Comment on lines +74 to +78
sections: allSections,
compareUrl: generateCompareUrl(currentVersion),
});
}
Comment on lines +69 to +82

currentVersion = versionMatch[1];
currentDate = versionMatch[2] || "";
allSections = [];
currentSectionLabel = null;
currentSectionEntries = [];
continue;
}

// Match section header: ### Added, ### Changed, etc.
const sectionMatch = trimmed.match(/^###\s+(\w+)$/);
if (sectionMatch && VALID_SECTIONS.has(sectionMatch[1])) {
// Save previous section if any
if (currentSectionLabel && currentSectionEntries.length > 0) {
allSections.push({
label: currentSectionLabel,
entries: currentSectionEntries,
});
}

currentSectionLabel = sectionMatch[1] as Section;
currentSectionEntries = [];
continue;
}

// Match bullet point: - **Title**: Description or - Description
if (trimmed.startsWith("- ") && currentSectionLabel) {
const content = trimmed.substring(2).trim();
const entry: ChangeEntry = {
text: formatEntryText(content),
};
currentSectionEntries.push(entry);
continue;
}
}

// Save final release
if (currentVersion && allSections.length > 0) {
releases.push({
version: currentVersion,
date: currentDate,
tag: currentVersion === "Unreleased" ? "unreleased" : releases.length === 0 ? "latest" : "",
summary: generateSummary(allSections),
sections: allSections,
compareUrl: generateCompareUrl(currentVersion),
});
}

return releases.filter((r) => r.version !== "Unreleased");
}

function formatEntryText(content: string): string {
// Remove markdown bold markers but keep the text: **Title** -> Title:
return content.replace(/\*\*([^*]+)\*\*:\s*/, "$1: ");
}

function generateSummary(sections: ReleaseSection[]): string {
// Create a brief summary from the first entry of the first section
if (sections.length === 0) return "Release";

const firstEntry = sections[0].entries[0];
if (!firstEntry) return "Release";

let text = firstEntry.text;
if (text.length > 100) {
text = text.substring(0, 97) + "...";
}
return text;
}

function generateCompareUrl(version: string): string {
if (version === "Unreleased") return "";

// Find the next version to create a comparison URL
// For now, use the tag-based comparison
return `https://github.com/EricCogen/GauntletCI/releases/tag/v${version}`;
Comment on lines +156 to +158
}
Loading