From 6510109896b5d900ad5dd401acf2fdfb2a71bc42 Mon Sep 17 00:00:00 2001 From: Yimin Jin Date: Wed, 27 May 2026 12:00:37 +0800 Subject: [PATCH 01/34] ci: bootstrap sample-catalog sync workflow Copy the sync-sample-catalog pipeline from microsoft-foundry-for-vscode so the foundry-toolkit repo can publish samples/hosted-agent/sample-catalog.json from upstream microsoft-foundry/foundry-samples. - .github/workflows/sync-sample-catalog.yml: workflow_dispatch-triggered job that runs the generator and opens a draft PR with the regenerated catalog. - .github/scripts/generate_sample_catalog.mjs: walks the upstream tree, derives displayName/protocol/requiresModel from each agent.yaml + manifest, applies sample-overrides.json, and LLM-generates descriptions. - samples/hosted-agent/sample-overrides.json: PM-managed per-path framework/requiresModel overrides. The workflow has only a workflow_dispatch trigger and requires repo secrets (SYNC_APP_ID, SYNC_APP_PRIVATE_KEY, AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY, AZURE_OPENAI_DEPLOYMENT) to run; secrets are intentionally not configured yet, so the workflow stays dormant until they are added. --- .github/scripts/generate_sample_catalog.mjs | 841 ++++++++++++++++++++ .github/workflows/sync-sample-catalog.yml | 186 +++++ samples/hosted-agent/sample-overrides.json | 8 + 3 files changed, 1035 insertions(+) create mode 100644 .github/scripts/generate_sample_catalog.mjs create mode 100644 .github/workflows/sync-sample-catalog.yml create mode 100644 samples/hosted-agent/sample-overrides.json diff --git a/.github/scripts/generate_sample_catalog.mjs b/.github/scripts/generate_sample_catalog.mjs new file mode 100644 index 0000000..2042068 --- /dev/null +++ b/.github/scripts/generate_sample_catalog.mjs @@ -0,0 +1,841 @@ +// @ts-check +/** + * Generate samples/hosted-agent/sample-catalog.json from the foundry-samples repository. + * + * Walks the hosted-agents tree in microsoft-foundry/foundry-samples once via + * the git-tree API, parses each sample's agent.yaml (+ optional + * agent.manifest.yaml) for protocol and model requirements, derives + * displayName from the directory name, and — when AZURE_OPENAI_* secrets are + * set — fills the description with a short LLM-generated sentence sourced + * from the sample's README.md. + * + * Usage: + * node generate_sample_catalog.mjs + * + * Environment variables: + * GITHUB_TOKEN Optional GitHub token for API authentication. + * REPO_ROOT Repository root (defaults to two levels up from this script). + * SAMPLES_REPO_URL Source repo URL (defaults to https://github.com/microsoft-foundry/foundry-samples/). + * AZURE_OPENAI_* Optional; when set, descriptions are LLM-generated. + */ + +import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs'; +import { join, dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const REPO_ROOT = process.env.REPO_ROOT || resolve(__dirname, '..', '..'); +const SAMPLES_REPO_URL = process.env.SAMPLES_REPO_URL || 'https://github.com/microsoft-foundry/foundry-samples/'; +const SAMPLES_REPO_API = 'https://api.github.com/repos/microsoft-foundry/foundry-samples'; +const OUTPUT_PATH = join(REPO_ROOT, 'samples', 'hosted-agent', 'sample-catalog.json'); +const OVERRIDES_PATH = join(REPO_ROOT, 'samples', 'hosted-agent', 'sample-overrides.json'); + +const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ''; + +const LANGUAGES = ['python', 'csharp']; +const FRAMEWORKS = ['agent-framework', 'bring-your-own']; + +/** @type {Record}>} */ +const DIMENSION_DEFAULTS = { + language: { + title: 'Select a Language', + placeholder: 'Choose the language for your agent', + options: { + python: 'Python', + csharp: 'C# (.NET)', + }, + }, + framework: { + title: 'Select a Framework', + placeholder: 'Choose the framework for your agent', + // Insertion order here drives the option order in the generated catalog + // (see buildDimensions). Keep the most actively promoted framework first. + options: { + 'copilot-sdk': 'Copilot SDK', + 'agent-framework': 'Agent Framework', + 'bring-your-own': 'Bring Your Own', + }, + }, + protocol: { + title: 'Select a Protocol', + placeholder: 'Choose the protocol for your agent', + options: { + responses: 'Responses API', + invocations: 'Invocations API', + }, + }, +}; + +const TEMPLATE_SELECTION = { + title: 'Select a Template', + placeholder: 'Choose a template for your agent', +}; + +// Path segments must be alphanumeric, hyphens, underscores, or dots +const SAFE_PATH_SEGMENT = /^[a-zA-Z0-9._-]+$/; + +/** + * Collected anomaly messages surfaced in CI step summary so reviewers do not + * silently merge a catalog with missing/derived data. Always populated, even + * when running locally without a step-summary file. + * @type {string[]} + */ +const warnings = []; + +/** + * Record a warning that should appear in the CI step summary, and mirror it + * to stderr for live log visibility. + * @param {string} message + */ +function warn(message) { + warnings.push(message); + console.warn(`WARN: ${message}`); +} + +/** + * @param {string} url + * @returns {Promise} + */ +async function fetchJson(url) { + /** @type {Record} */ + const headers = { + 'User-Agent': 'microsoft-foundry-for-vscode-sync', + Accept: 'application/vnd.github+json', + }; + if (GITHUB_TOKEN) { + headers['Authorization'] = `Bearer ${GITHUB_TOKEN}`; + } + const response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error(`HTTP ${response.status} for ${url}`); + } + return response.json(); +} + +/** + * @param {string} url + * @returns {Promise} + */ +async function fetchText(url) { + /** @type {Record} */ + const headers = { 'User-Agent': 'microsoft-foundry-for-vscode-sync' }; + if (GITHUB_TOKEN) { + headers['Authorization'] = `Bearer ${GITHUB_TOKEN}`; + } + const response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error(`HTTP ${response.status} for ${url}`); + } + return response.text(); +} + +/** + * Validate that a path segment is safe: no traversal, no special chars, no + * leading-dot directories (e.g. `.claude`, `.github`) which are tooling + * metadata, not sample templates. + * @param {string} segment + * @returns {boolean} + */ +function isSafePathSegment(segment) { + if (segment === '.' || segment === '..' || segment.startsWith('.')) { + return false; + } + return SAFE_PATH_SEGMENT.test(segment); +} + +/** + * Parse the commit SHA from the CLI argument. + * @returns {string} + */ +function parseCommitShaArg() { + const sha = process.argv[2] || ''; + if (!sha) { + console.error('Usage: node generate_sample_catalog.mjs '); + process.exit(1); + } + if (!/^[0-9a-f]{7,40}$/i.test(sha)) { + throw new Error(`Invalid commit SHA: "${sha}"`); + } + return sha; +} + +/** + * Fetch the full recursive git tree for a ref. One API call covers the entire + * `samples/` hierarchy, which is much cheaper (and more accurate) than walking + * the `/contents/` endpoint level-by-level — and it lets us discover samples + * at arbitrary nesting depths (e.g. `bring-your-own/voicelive/