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 .agents/skills/update-samples/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ After running the script:
| Variable | Required | Description |
|---|---|---|
| `GITHUB_TOKEN` | No | GitHub personal access token for higher API rate limits (60 → 5000 requests/hour) |
| `SAMPLES_BRANCH` | No | Override the `microsoft/aspire-samples` ref to fetch from (default `main`). Useful for staging `samples.json` against a still-open upstream PR — set to the PR's head ref. The generated `href` fields stay anchored to `main` so links remain valid after the PR branch is deleted. |

### 7. Integration with build

Expand Down
33 changes: 27 additions & 6 deletions src/frontend/scripts/update-samples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,32 @@ import { pipeline } from 'stream/promises';
import fetch from 'node-fetch';

const REPO = 'microsoft/aspire-samples';
const BRANCH = 'main';
const DEFAULT_BRANCH = 'main';
// `BRANCH` controls which ref of `microsoft/aspire-samples` is fetched (README,
// AppHost code, raw assets, tree listing). The override exists so contributors
// can stage `samples.json` against a still-open upstream PR by running the
// script with `SAMPLES_BRANCH=<head-ref> pnpm run update:samples` — the output
// is identical to what the next normal run will produce after that PR merges,
// so the staged data PR is idempotent with the upstream merge.
const BRANCH = process.env.SAMPLES_BRANCH ?? DEFAULT_BRANCH;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 1dfb879.

I went with two encoded variants instead of a single one:

  • BRANCH_PATH = BRANCH.split('/').map(encodeURIComponent).join('/') — per-segment encoding. Preserves / boundaries (which GitHub accepts in path positions for both raw.githubusercontent.com and api.github.com/.../git/trees/...) while still escaping #/?/%/&/etc. within each segment.
  • BRANCH_QUERY = encodeURIComponent(BRANCH) — full encoding for the ?ref= query param on the contents API (where / must be %2F).

RAW_BASE and the git/trees endpoint now use BRANCH_PATH; the contents?ref= call uses BRANCH_QUERY. Verified end-to-end against the live API with ievangelist/dapine-aspire-api-updates — both endpoints return the expected payloads.

// PR head refs frequently contain `/` (e.g. `feature/foo`,
// `user/dapine-aspire-api-updates`). GitHub accepts `/` in path positions for
// the raw, contents, and git/trees endpoints, but other URL-significant
// characters in a ref (`#`, `?`, `%`, `&`, etc.) need to be percent-encoded.
// Encoding per `/`-separated segment preserves the `/` boundaries while
// safely escaping every other special character within each segment.
const BRANCH_PATH = BRANCH.split('/').map(encodeURIComponent).join('/');
const BRANCH_QUERY = encodeURIComponent(BRANCH);
const SAMPLES_DIR = 'samples';
const OUTPUT_PATH = './src/data/samples.json';
const ASSETS_DIR = './src/assets/samples';
const ASSETS_IMPORT_PREFIX = '~/assets/samples';
const GITHUB_API = 'https://api.github.com';
const RAW_BASE = `https://raw.githubusercontent.com/${REPO}/${BRANCH}`;
const TREE_BASE = `https://github.com/${REPO}/tree/${BRANCH}`;
const RAW_BASE = `https://raw.githubusercontent.com/${REPO}/${BRANCH_PATH}`;
// `TREE_BASE` is intentionally pinned to `main` (never the override) so the
// generated `href` fields on each sample stay valid after a PR branch goes
// away. The branch override is for fetching content, not for cementing links.
const TREE_BASE = `https://github.com/${REPO}/tree/${DEFAULT_BRANCH}`;
const LEGACY_DOCS_HOST = 'https://learn.microsoft.com';
const ASPIRE_DOC_URL_REWRITES = [
[
Expand Down Expand Up @@ -364,7 +382,7 @@ async function fetchText(url: string): Promise<string | null> {

async function listSampleDirs(): Promise<string[]> {
const contents = await fetchJson<GitHubContentEntry[]>(
`${GITHUB_API}/repos/${REPO}/contents/${SAMPLES_DIR}?ref=${BRANCH}`
`${GITHUB_API}/repos/${REPO}/contents/${SAMPLES_DIR}?ref=${BRANCH_QUERY}`
);

return contents
Expand All @@ -380,7 +398,7 @@ async function listSampleDirs(): Promise<string[]> {
*/
async function fetchSamplePaths(): Promise<Map<string, string[]>> {
const tree = await fetchJson<GitTreeResponse>(
`${GITHUB_API}/repos/${REPO}/git/trees/${BRANCH}?recursive=1`
`${GITHUB_API}/repos/${REPO}/git/trees/${BRANCH_PATH}?recursive=1`
);

if (tree.truncated) {
Expand Down Expand Up @@ -471,7 +489,10 @@ async function processSample(
}

async function main(): Promise<void> {
console.log(`📦 Fetching sample directories from ${REPO}...`);
console.log(`📦 Fetching sample directories from ${REPO}@${BRANCH}...`);
if (BRANCH !== DEFAULT_BRANCH) {
console.log(` (branch override via SAMPLES_BRANCH; href links stay anchored to ${DEFAULT_BRANCH})`);
}
const [dirs, pathsBySample] = await Promise.all([listSampleDirs(), fetchSamplePaths()]);
console.log(`📂 Found ${dirs.length} sample directories`);

Expand Down
Loading