Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
54 changes: 54 additions & 0 deletions .github/workflows/changelog-snapshot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Snapshot Pyth symbols for Change Log

on:
schedule:
# Daily at 00:30 UTC. Slight offset from midnight to avoid contention
# and to ensure any in-progress upstream writes have settled.
- cron: "30 0 * * *"
workflow_dispatch: {}

permissions:
contents: write

jobs:
snapshot:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version-file: "package.json"
cache: pnpm

- name: Install dependencies for developer-hub
working-directory: apps/developer-hub
run: pnpm install --frozen-lockfile --filter @pythnetwork/developer-hub...

- name: Capture today's snapshot and diff
working-directory: apps/developer-hub
run: pnpm snapshot:changelog

- name: Verify generator runs cleanly
working-directory: apps/developer-hub
run: pnpm generate:changelog

- name: Commit and push if changed
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add apps/developer-hub/data/latest-snapshot.json \
apps/developer-hub/data/changelog-diffs/
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated

if git diff --cached --quiet; then
echo "No baseline change and no new diff — nothing to commit."
exit 0
fi

DATE=$(date -u +%Y-%m-%d)
git commit -m "chore(developer-hub): daily change-log diff ${DATE}"
git push
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
3 changes: 3 additions & 0 deletions apps/developer-hub/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
dist/
next-env.d.ts

# Auto-generated by `pnpm generate:changelog` from data/changelog-snapshots/.
src/components/ChangeLog/generated-data.ts
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Commit or pre-generate required change-log module

src/components/ChangeLog/generated-data.ts is now ignored, but data.ts imports it unconditionally, so a fresh checkout does not contain a resolvable module until someone manually runs pnpm generate:changelog. In this repo the normal dev path (start:dev -> next dev) does not generate that file first, which causes a module-not-found failure for the new page in clean environments.

Useful? React with 👍 / 👎.


# Ai Migration
.ai/**
9 changes: 9 additions & 0 deletions apps/developer-hub/content/docs/price-feeds/changelog.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: Change Log
description: Daily record of status transitions on Pyth price feeds — additions, activations, upcoming expirations, and removals.
slug: /price-feeds/changelog
---

import { ChangeLog } from "../../../src/components/ChangeLog";

<ChangeLog />
2 changes: 1 addition & 1 deletion apps/developer-hub/content/docs/price-feeds/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"root": true,
"title": "Price Feeds",
"description": "Real-time financial market data",
"pages": ["core", "pro"],
"pages": ["core", "pro", "changelog"],
"defaultOpen": true,
"icon": "ChartLine"
}
87 changes: 87 additions & 0 deletions apps/developer-hub/data/changelog-diffs/2026-05-08.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"date": "2026-05-08",
"label": "Friday",
"summary": {
"added": 6,
"went_live": 2,
"expiring": 0,
"removed": 0
},
"hero": "6 new feeds announced, 2 went live.",
"events": [
{
"id": "Equity.US.IGE/USD",
"lazerId": 2795,
"asset": "Ishares North American Natural Resources Etf",
"assetType": "equity",
"date": "2026-05-08",
"quote": "USD",
"hermesId": "c0948c8763aafa254159835fac7d9cc1f65cb2c370791d7fa52493e0bfe974bd",
"changeType": "went_live"
},
{
"id": "Equity.US.TER/USD",
"lazerId": 1420,
"asset": "Teradyne Inc",
"assetType": "equity",
"date": "2026-05-08",
"quote": "USD",
"hermesId": "58ab181e7512766728d2cc3581839bbb913e6cd24457ba422cbe2a33df64416e",
"changeType": "went_live"
},
{
"id": "Crypto.AFSUI/SUI.RR",
"lazerId": 3245,
"asset": "Aftermath Staked Sui",
"assetType": "crypto-redemption-rate",
"date": "2026-05-08",
"quote": "SUI",
"changeType": "added"
},
{
"id": "Crypto.HASUI/SUI.RR",
"lazerId": 3244,
"asset": "Haedal Staked Sui",
"assetType": "crypto-redemption-rate",
"date": "2026-05-08",
"quote": "SUI",
"changeType": "added"
},
{
"id": "Crypto.EXUSDC/USDC.RR",
"lazerId": 3243,
"asset": "Exceed Usd Coin",
"assetType": "crypto-redemption-rate",
"date": "2026-05-08",
"quote": "USDC",
"changeType": "added"
},
{
"id": "Crypto.LIMUSD/USDC.RR",
"lazerId": 3242,
"asset": "Liminal Us Dollar",
"assetType": "crypto-redemption-rate",
"date": "2026-05-08",
"quote": "USDC",
"changeType": "added"
},
{
"id": "Crypto.HEMIBTC/BTC.RR",
"lazerId": 3241,
"asset": "Hemi Bitcoin",
"assetType": "crypto-redemption-rate",
"date": "2026-05-08",
"quote": "BTC",
"changeType": "added"
},
{
"id": "Equity.US.DRAM/USD",
"lazerId": 3240,
"asset": "Roundhill Memory Etf",
"assetType": "equity",
"date": "2026-05-08",
"quote": "USD",
"changeType": "added"
}
]
}
1 change: 1 addition & 0 deletions apps/developer-hub/data/latest-snapshot.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/developer-hub/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ const config = {
},

{
source: String.raw`/price-feeds/:path((?!core(?:/|$|\.mdx?$)|pro(?:/|$|\.mdx?$)|hip-3-service(?:/|$|\.mdx?$)).*)`,
source: String.raw`/price-feeds/:path((?!core(?:/|$|\.mdx?$)|pro(?:/|$|\.mdx?$)|hip-3-service(?:/|$|\.mdx?$)|changelog(?:/|$|\.mdx?$)).*)`,
destination: "/price-feeds/core/:path",
permanent: true,
},
Expand Down
2 changes: 2 additions & 0 deletions apps/developer-hub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@
"build:analyze": "ANALYZE=true next build",
"count:llm-tokens": "tsx ./scripts/count-llm-tokens.ts",
"fix:lint:stylelint": "stylelint --fix 'src/**/*.scss'",
"generate:changelog": "tsx ./scripts/generate-changelog.ts",
"generate:docs": "tsx ./scripts/generate-docs.ts",
"snapshot:changelog": "tsx ./scripts/snapshot-and-diff.ts",
"start:dev": "next dev --port 3627",
"start:prod": "next start --port 3627",
"test:lint:stylelint": "stylelint 'src/**/*.scss' --max-warnings 0",
Expand Down
182 changes: 182 additions & 0 deletions apps/developer-hub/scripts/changelog-lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/**
* Shared types and helpers for the Change Log snapshot/diff/generate
* scripts. Kept outside `src/` so it can be imported by both
* `snapshot-and-diff.ts` (writes daily diff JSONs) and
* `generate-changelog.ts` (bundles diffs into the rendered TS module).
*/

export type Symbol = {
pyth_lazer_id: number;
name: string;
symbol: string;
description?: string | null;
asset_type: string;
state: string;
hermes_id?: string | null;
quote_currency?: string | null;
};

export type ChangeType = "added" | "went_live" | "expiring_soon" | "removed";

export type Entry = {
id: string;
lazerId: number;
asset: string;
assetType: string;
quote?: string;
hermesId?: string;
changeType: ChangeType;
date: string;
};

export type DaySummary = {
added: number;
went_live: number;
expiring: number;
removed: number;
};

export type Day = {
date: string;
label: string;
summary: DaySummary;
hero: string;
events: Entry[];
};

// ─── Helpers ─────────────────────────────────────────────────────────────

export const isDeprecated = (s: Symbol): boolean => {
const desc = (s.description ?? "").toUpperCase();
return (
desc.includes("DEPRECATED") ||
s.asset_type.toUpperCase().includes("DEPRECATED")
);
};

export const toTitleCase = (raw: string): string =>
raw.toLowerCase().replaceAll(/\b([a-z])/g, (_, c: string) => c.toUpperCase());

export const extractAssetName = (s: Symbol): string => {
const desc = s.description?.replace(/^DEPRECATED FEED\s*-?\s*/i, "").trim();
if (desc && desc.length > 0) {
const [base] = desc.split("/");
return toTitleCase((base ?? "").trim());
}
const sym = s.symbol.split(".").pop() ?? s.symbol;
return sym.split("/")[0] ?? sym;
};

export const dayLabel = (iso: string): string => {
const d = new Date(`${iso}T00:00:00Z`);
return d.toLocaleDateString("en-US", { weekday: "long", timeZone: "UTC" });
};

export const synthesizeHero = (s: DaySummary): string => {
const parts: string[] = [];
if (s.added > 0) {
parts.push(`${s.added.toString()} new feed${s.added === 1 ? "" : "s"} announced`);
}
if (s.went_live > 0) {
parts.push(`${s.went_live.toString()} went live`);
}
if (s.removed > 0) {
parts.push(`${s.removed.toString()} deactivated or deprecated`);
}
if (parts.length === 0) {
return "A quiet day on Pyth. No status transitions on any price feed.";
}
return `${parts.join(", ")}.`;
};

const baseEntry = (s: Symbol, date: string): Omit<Entry, "changeType"> => ({
id: s.symbol,
lazerId: s.pyth_lazer_id,
asset: extractAssetName(s),
assetType: s.asset_type,
date,
...(s.quote_currency == null ? {} : { quote: s.quote_currency }),
...(s.hermes_id == null ? {} : { hermesId: s.hermes_id }),
});

/**
* Diff one pair of snapshots into a single Day entry. Diff rules:
*
* - new pyth_lazer_id today (not in yesterday, not deprecated) → added
* - missing pyth_lazer_id today (was in yesterday) → removed
* - state coming_soon → stable → went_live
* - state stable → inactive → removed
* - description gains "DEPRECATED FEED" → removed
*
* `expiring_soon` is not synthesizable from the symbols API alone
* (no scheduled deactivation date is exposed).
*/
export const diffPair = (
yesterday: Symbol[],
today: Symbol[],
date: string,
): Day => {
const ymap = new Map<number, Symbol>(
yesterday.map((s) => [s.pyth_lazer_id, s]),
);
const tmap = new Map<number, Symbol>(
today.map((s) => [s.pyth_lazer_id, s]),
);

const events: Entry[] = [];

for (const s of today) {
if (!ymap.has(s.pyth_lazer_id) && !isDeprecated(s)) {
events.push({ ...baseEntry(s, date), changeType: "added" });
}
}

for (const s of yesterday) {
if (!tmap.has(s.pyth_lazer_id)) {
events.push({ ...baseEntry(s, date), changeType: "removed" });
}
}

for (const t of today) {
const y = ymap.get(t.pyth_lazer_id);
if (!y) continue;
const prev = y.state;
const next = t.state;
const yDep = isDeprecated(y);
const tDep = isDeprecated(t);

if (prev === "coming_soon" && next === "stable") {
events.push({ ...baseEntry(t, date), changeType: "went_live" });
} else if (prev === "stable" && next === "inactive") {
events.push({ ...baseEntry(t, date), changeType: "removed" });
} else if (!yDep && tDep) {
events.push({ ...baseEntry(t, date), changeType: "removed" });
}
}

const order: Record<ChangeType, number> = {
removed: 0,
went_live: 1,
added: 2,
expiring_soon: 3,
};
events.sort((a, b) => {
const ord = order[a.changeType] - order[b.changeType];
return ord === 0 ? b.lazerId - a.lazerId : ord;
});

const summary: DaySummary = {
added: events.filter((e) => e.changeType === "added").length,
went_live: events.filter((e) => e.changeType === "went_live").length,
expiring: 0,
removed: events.filter((e) => e.changeType === "removed").length,
};

return {
date,
label: dayLabel(date),
summary,
hero: synthesizeHero(summary),
events,
};
};
Loading
Loading