From ce671c72191359559a0d39a5112c0b64c51683c8 Mon Sep 17 00:00:00 2001 From: Mara Nikola Kiefer Date: Fri, 17 Jul 2026 13:43:41 +0200 Subject: [PATCH 1/5] docs: add workshop view --- docs/astro.config.mjs | 1 + docs/package.json | 5 +- docs/scripts/sync-workshop-content.js | 179 +++ docs/src/components/CustomHeader.astro | 2 + .../workshop/WorkshopExperience.astro | 1405 +++++++++++++++++ docs/src/content/docs/index.mdx | 11 +- docs/src/generated/workshop-content.ts | 651 ++++++++ docs/src/lib/workshop/manifest.ts | 99 ++ docs/src/lib/workshop/routes.ts | 207 +++ docs/src/pages/workshop.astro | 21 + 10 files changed, 2576 insertions(+), 5 deletions(-) create mode 100644 docs/scripts/sync-workshop-content.js create mode 100644 docs/src/components/workshop/WorkshopExperience.astro create mode 100644 docs/src/generated/workshop-content.ts create mode 100644 docs/src/lib/workshop/manifest.ts create mode 100644 docs/src/lib/workshop/routes.ts create mode 100644 docs/src/pages/workshop.astro diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 69867e5b6a0..842f6cf398d 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -304,6 +304,7 @@ export default defineConfig({ label: 'Setup', items: [ { label: 'Quick Start', link: '/setup/quick-start/' }, + { label: 'Workshop', link: '/workshop/' }, { label: 'Creating Workflows', link: '/setup/creating-workflows/' }, { label: 'CLI Commands', link: '/setup/cli/' }, ], diff --git a/docs/package.json b/docs/package.json index 6b4b4960086..f28795b0db7 100644 --- a/docs/package.json +++ b/docs/package.json @@ -7,14 +7,15 @@ }, "scripts": { "dev": "astro dev", - "predev": "npm run build:slides", + "predev": "npm run generate-workshop-content && npm run build:slides", "start": "astro dev", - "prebuild": "npm run generate-agent-factory && npm run generate-model-tables && npm run build:slides", + "prebuild": "npm run generate-workshop-content && npm run generate-agent-factory && npm run generate-model-tables && npm run build:slides", "build": "rm -rf dist && astro build", "build:slides": "node ../scripts/ensure-docs-slide-pdf.js", "preview": "astro preview", "astro": "astro", "validate-links": "astro build", + "generate-workshop-content": "node ./scripts/sync-workshop-content.js", "generate-agent-factory": "cd .. && node scripts/generate-agent-factory.js", "generate-model-tables": "cd .. && node scripts/generate-model-tables.js", "test": "playwright test", diff --git a/docs/scripts/sync-workshop-content.js b/docs/scripts/sync-workshop-content.js new file mode 100644 index 00000000000..add807f5115 --- /dev/null +++ b/docs/scripts/sync-workshop-content.js @@ -0,0 +1,179 @@ +import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; + +const docsRoot = resolve(import.meta.dirname, '..'); +const generatedDir = resolve(docsRoot, 'src/generated'); +const outputFile = join(generatedDir, 'workshop-content.ts'); +const workshopRepo = process.env.GH_AW_WORKSHOP_REPO || 'githubnext/gh-aw-workshop'; +const workshopRef = process.env.GH_AW_WORKSHOP_REF || 'main'; +const localWorkshopSourceDir = process.env.GH_AW_WORKSHOP_SOURCE_DIR; +const publicWorkshopBase = `https://github.com/${workshopRepo}`; +const publicWorkshopTreeUrl = `${publicWorkshopBase}/tree/${workshopRef}/workshop`; +const publicWorkshopBlobBaseUrl = `${publicWorkshopBase}/blob/${workshopRef}/workshop/`; +const publicWorkshopRawBaseUrl = `https://raw.githubusercontent.com/${workshopRepo}/${workshopRef}/workshop/`; + +function getGitHubHeaders() { + const headers = { + Accept: 'application/vnd.github+json', + 'User-Agent': 'gh-aw-docs-workshop-sync', + }; + + if (process.env.GITHUB_TOKEN) { + headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; + } + + return headers; +} + +async function fetchJson(url) { + const response = await fetch(url, { headers: getGitHubHeaders() }); + if (!response.ok) { + throw new Error(`GET ${url} failed with ${response.status} ${response.statusText}`); + } + return response.json(); +} + +async function fetchText(url) { + const response = await fetch(url, { headers: getGitHubHeaders() }); + if (!response.ok) { + throw new Error(`GET ${url} failed with ${response.status} ${response.statusText}`); + } + return response.text(); +} + +async function loadRemoteWorkshopEntries() { + const contentsUrl = `https://api.github.com/repos/${workshopRepo}/contents/workshop?ref=${encodeURIComponent(workshopRef)}`; + const contents = await fetchJson(contentsUrl); + if (!Array.isArray(contents)) { + throw new Error(`Expected ${contentsUrl} to return a directory listing.`); + } + + const markdownFiles = contents + .filter((item) => item.type === 'file' && item.name.endsWith('.md') && item.download_url) + .sort((left, right) => left.name.localeCompare(right.name, undefined, { numeric: true })); + + return Promise.all(markdownFiles.map(async (item) => ({ + id: item.name, + body: await fetchText(item.download_url), + }))); +} + +function loadLocalWorkshopEntries(sourceDir) { + if (!existsSync(sourceDir)) { + throw new Error(`GH_AW_WORKSHOP_SOURCE_DIR does not exist: ${sourceDir}`); + } + + return readdirSync(sourceDir) + .filter((name) => name.endsWith('.md')) + .sort((left, right) => left.localeCompare(right, undefined, { numeric: true })) + .map((name) => ({ + id: name, + body: readFileSync(join(sourceDir, name), 'utf8'), + })); +} + +function stripMarkdown(value) { + return String(value) + .replace(/!\[([^\]]*)\]\([^)]+\)/gu, '$1') + .replace(/\[([^\]]+)\]\([^)]+\)/gu, '$1') + .replace(/`([^`]+)`/gu, '$1') + .replace(/\*\*([^*]+)\*\*/gu, '$1') + .replace(/_([^_]+)_/gu, '$1') + .replace(/<[^>]+>/gu, '') + .trim(); +} + +function extractTitle(body, fallbackId) { + const headingMatch = body.match(/^#\s+(.+)$/mu); + if (headingMatch) return stripMarkdown(headingMatch[1]); + + return normalizeStepId(fallbackId) + .split('-') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(' '); +} + +function extractSummary(body) { + const lines = body + .replace(/^#\s+.+$/mu, '') + .split('\n') + .map((line) => line.trim()) + .filter((line) => { + return Boolean(line) + && !line.startsWith('![') + && !line.startsWith('>') + && !line.startsWith('|') + && !line.startsWith('```') + && !line.startsWith('- ') + && !line.startsWith('* ') + && !line.startsWith('## ') + && !line.startsWith('### ') + && !line.startsWith('_'); + }); + + return stripMarkdown(lines[0] ?? 'Continue with this workshop step inside the docs.'); +} + +function addEntryMetadata(entries) { + return entries.map((entry) => ({ + ...entry, + title: extractTitle(entry.body, entry.id), + summary: extractSummary(entry.body), + })); +} + +async function loadWorkshopEntries() { + if (localWorkshopSourceDir) { + const sourceDir = resolve(localWorkshopSourceDir); + return { + source: sourceDir, + entries: loadLocalWorkshopEntries(sourceDir), + }; + } + + try { + return { + source: publicWorkshopTreeUrl, + entries: await loadRemoteWorkshopEntries(), + }; + } catch (error) { + if (existsSync(outputFile)) { + console.warn(`Could not fetch ${workshopRepo}@${workshopRef}; using existing generated content. ${error.message}`); + process.exit(0); + } + + throw error; + } +} + +const { source, entries } = await loadWorkshopEntries(); +const workshopEntries = addEntryMetadata(entries); + +mkdirSync(generatedDir, { recursive: true }); + +const output = `${[ + '// Generated by docs/scripts/sync-workshop-content.js', + `// Source: ${source}`, + '// Do not edit by hand.', + '', + 'export const workshopSource = {', + `\trepo: ${JSON.stringify(workshopRepo)},`, + `\tref: ${JSON.stringify(workshopRef)},`, + `\ttreeUrl: ${JSON.stringify(publicWorkshopTreeUrl)},`, + `\tgithubBaseUrl: ${JSON.stringify(publicWorkshopBlobBaseUrl)},`, + `\trawBaseUrl: ${JSON.stringify(publicWorkshopRawBaseUrl)},`, + '};', + '', + 'export type WorkshopContentEntry = {', + "\tid: string;", + "\ttitle: string;", + "\tsummary: string;", + "\tbody: string;", + '};', + '', + `export const workshopContent: WorkshopContentEntry[] = ${JSON.stringify(workshopEntries, null, '\t')};`, + '', +].join('\n')}`; + +writeFileSync(outputFile, output, 'utf8'); +console.log(`Generated workshop content from ${source}: ${outputFile}`); \ No newline at end of file diff --git a/docs/src/components/CustomHeader.astro b/docs/src/components/CustomHeader.astro index 8ada6d93c40..741e4224414 100644 --- a/docs/src/components/CustomHeader.astro +++ b/docs/src/components/CustomHeader.astro @@ -8,6 +8,7 @@ const base = import.meta.env.BASE_URL;