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
32 changes: 32 additions & 0 deletions .github/workflows/meeting-recordings-refresh.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Refresh meeting recordings

on:
workflow_dispatch:
schedule:
- cron: '17 12 * * *'

permissions:
contents: write

env:
NETLIFY_BUILD_HOOK_URL: ${{ secrets.NETLIFY_BUILD_HOOK_URL }}

jobs:
refresh:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20.11.1'
cache: npm
- run: npm ci
- run: npx tsx scripts/update-meeting-recordings.ts
- name: Commit recording feed snapshot
uses: stefanzweifel/git-auto-commit-action@v6
with:
commit_message: 'docs: refresh community meeting recordings'
file_pattern: data/meeting-recordings.json
- name: Trigger Netlify rebuild
if: env.NETLIFY_BUILD_HOOK_URL != ''
run: curl --fail -sS -X POST "$NETLIFY_BUILD_HOOK_URL"
16 changes: 16 additions & 0 deletions data/meeting-recordings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"updatedAt": "2026-09-24T11:38:59.783Z",
"source": "https://www.youtube.com/feeds/videos.xml?channel_id=UCIA3fKJFv2nLoG6vKK65xLg",
"channelId": "UCIA3fKJFv2nLoG6vKK65xLg",
"recordings": [
{
"id": "4pL2_thwKDA",
"title": "Hive Commons 20260910 Meeting Recording",
"publishedAt": "2026-09-10T16:07:23+00:00",
"updatedAt": "2026-09-10T16:35:39+00:00",
"url": "https://www.youtube.com/watch?v=4pL2_thwKDA",
"thumbnail": "https://i.ytimg.com/vi/4pL2_thwKDA/hqdefault.jpg",
"description": "our first meeting!"
}
]
}
2 changes: 1 addition & 1 deletion docs/content/community/join-hive-commons.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,4 @@ Not ready to bring a whole project? You're still welcome:
- **Join the conversation** on
[hivecommons-dev@googlegroups.com](https://groups.google.com/g/hivecommons-dev),
[Discord](https://hivecommons.dev/discord), or the biweekly
[community meeting](https://hivecommons.dev/agenda).
[community meeting](/docs/community/meetings).
3 changes: 3 additions & 0 deletions docs/content/community/meetings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Community meetings

<MeetingsPage />
2 changes: 1 addition & 1 deletion docs/content/community/what-is-hive-commons.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ contributions. Hive Commons has applied for the

- [Governance](https://github.com/hivecommons/.github/blob/main/GOVERNANCE.md) · [Maintainers](https://github.com/hivecommons/.github/blob/main/MAINTAINERS.md) · [Code of Conduct](https://github.com/hivecommons/.github/blob/main/CODE_OF_CONDUCT.md) · [Security policy](https://github.com/hivecommons/.github/blob/main/SECURITY.md)
- **Mailing list:** [hivecommons-dev@googlegroups.com](https://groups.google.com/g/hivecommons-dev) — joining adds the community meeting to your calendar
- **Community meeting:** every other Thursday, 10:00 AM ET — [agenda and notes](https://hivecommons.dev/agenda) · [recordings](https://hivecommons.dev/tv)
- **Community meeting:** every other Thursday, 10:00 AM ET — [meeting page](/docs/community/meetings) · [agenda and notes](https://hivecommons.dev/agenda) · [recordings](https://hivecommons.dev/tv)
- **Chat:** [Discord](https://hivecommons.dev/discord)
- **Website:** [hivecommons.dev](https://hivecommons.dev)

Expand Down
2 changes: 1 addition & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const nextConfig: NextConfig = {
"font-src 'self' data:",
"connect-src 'self' https://api.github.com https://www.google-analytics.com",
"worker-src blob:",
"frame-src 'none'",
"frame-src https://www.youtube-nocookie.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"prebuild": "(tsx scripts/sync-hive-docs.ts || echo \"hive docs sync unavailable - using committed content\") && (tsx scripts/sync-sibling-docs.ts || echo \"sibling docs sync unavailable - using committed content\") && tsx scripts/generate-shared-config.ts",
"prebuild": "(tsx scripts/update-meeting-recordings.ts || echo \"meeting recordings refresh unavailable - using committed data\") && (tsx scripts/sync-hive-docs.ts || echo \"hive docs sync unavailable - using committed content\") && (tsx scripts/sync-sibling-docs.ts || echo \"sibling docs sync unavailable - using committed content\") && tsx scripts/generate-shared-config.ts",
"build": "next build",
"start": "next start",
"lint": "eslint src/",
Expand Down
109 changes: 109 additions & 0 deletions scripts/update-meeting-recordings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import fs from 'fs'
import path from 'path'

const DATA_PATH = path.join(process.cwd(), 'data', 'meeting-recordings.json')
const FEED_URL = 'https://www.youtube.com/feeds/videos.xml?channel_id=UCIA3fKJFv2nLoG6vKK65xLg'
const CHANNEL_ID = 'UCIA3fKJFv2nLoG6vKK65xLg'
const TITLE_RE = /community meeting|community call|meeting recording/i

type Recording = {
id: string
title: string
publishedAt: string
updatedAt?: string
url: string
thumbnail: string
description?: string
duration?: string
}

type Store = {
updatedAt: string
source: string
channelId: string
recordings: Recording[]
}

function textBetween(input: string, tag: string): string | undefined {
const match = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`).exec(input)
return match?.[1]?.trim()
}

function attr(input: string, tag: string, name: string): string | undefined {
const match = new RegExp(`<${tag}[^>]*\\s${name}="([^"]+)"[^>]*>`).exec(input)
return match?.[1]
}

function decodeXml(input = ''): string {
return input
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
}

function parseFeed(xml: string): Recording[] {
const entries = xml.match(/<entry>[\s\S]*?<\/entry>/g) ?? []
return entries.flatMap(entry => {
const id = textBetween(entry, 'yt:videoId')
const title = decodeXml(textBetween(entry, 'title'))
if (!id || !title || !TITLE_RE.test(title)) return []

return [{
id,
title,
publishedAt: textBetween(entry, 'published') ?? '',
updatedAt: textBetween(entry, 'updated'),
url: attr(entry, 'link', 'href') ?? `https://www.youtube.com/watch?v=${id}`,
thumbnail: attr(entry, 'media:thumbnail', 'url')?.replace('https://i1.ytimg.com/', 'https://i.ytimg.com/') ?? `https://i.ytimg.com/vi/${id}/hqdefault.jpg`,
description: decodeXml(textBetween(entry, 'media:description')),
}]
})
}

function readExisting(): Store {
try {
return JSON.parse(fs.readFileSync(DATA_PATH, 'utf8')) as Store
} catch {
return { updatedAt: new Date(0).toISOString(), source: FEED_URL, channelId: CHANNEL_ID, recordings: [] }
}
}

async function main() {
const existing = readExisting()
let fetched: Recording[] = []

try {
const response = await fetch(FEED_URL)
if (!response.ok) throw new Error(`YouTube feed returned ${response.status}`)
fetched = parseFeed(await response.text())
} catch (error) {
console.warn(`meeting recordings feed unavailable; keeping committed data: ${error instanceof Error ? error.message : String(error)}`)
}

const byId = new Map<string, Recording>()
for (const recording of existing.recordings) byId.set(recording.id, recording)
for (const recording of fetched) byId.set(recording.id, { ...byId.get(recording.id), ...recording })

const recordings = [...byId.values()].sort((a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt))
const existingStable = JSON.stringify(existing.recordings)
const nextStable = JSON.stringify(recordings)

if (existingStable === nextStable && existing.source === FEED_URL && existing.channelId === CHANNEL_ID) {
console.log(`meeting recordings: ${existing.recordings.length} (unchanged)`)
return
}

const next: Store = {
updatedAt: new Date().toISOString(),
source: FEED_URL,
channelId: CHANNEL_ID,
recordings,
}

fs.writeFileSync(DATA_PATH, `${JSON.stringify(next, null, 2)}\n`)
console.log(`meeting recordings: ${next.recordings.length}`)
}

void main()
15 changes: 15 additions & 0 deletions src/app/docs/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { sanitizeHtmlForMdx, removeCommentPatterns } from '@/lib/sanitizeHtml'
import { rewriteRelativeImagePaths } from '@/lib/rewriteImagePaths'
import { buildPageMap, docsContentPath, getContentPath, isGeneralSectionFile } from '../page-map'
import { CURRENT_VERSION, type ProjectId } from '@/config/versions'
import { MeetingsPage } from '@/components/community/MeetingsPage'
import fs from 'fs'
import path from 'path'
import { notFound } from 'next/navigation'
Expand Down Expand Up @@ -207,6 +208,20 @@ export default async function DocPage({ params }: Props) {
convertHtmlScriptsToJsxComments
})

if (filePath === 'community/meetings.md') {
return (
<Wrapper
toc={[]}
metadata={{ title: 'Community meetings' }}
filePath={filePath}
projectId={projectId ?? 'hive'}
sourceUrl={sourceUrl}
>
<MeetingsPage />
</Wrapper>
)
}

let evaluated: ReturnType<typeof evaluate> | null = null
let compilationFailed = false
try {
Expand Down
56 changes: 54 additions & 2 deletions src/app/docs/page-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,17 @@ const NAV_STRUCTURE_HIVE: Array<{ title: string; items: NavItem[] }> = [
title: 'Overview',
items: [
{ 'Introduction': 'readme.md' },
{ 'Getting Started': 'getting-started.md' },
{ 'Architecture': 'architecture.md' },
{ 'Roadmap': 'roadmap.md' },
{ 'Landscape': 'landscape.md' },
]
},
{
title: 'Getting started',
items: [
{ 'Manual provisioning': 'manual-provisioning.md' },
{ 'Network admin requirement': 'net-admin-requirement.md' },
]
},
{
Expand All @@ -99,8 +109,47 @@ const NAV_STRUCTURE_HIVE: Array<{ title: string; items: NavItem[] }> = [
{ 'Agent Definition YAML': 'agent-definition-yaml.md' },
{ 'Variable Substitution': 'variable-substitution.md' },
{ 'Governor': 'governor.md' },
{ 'hivectl': 'hivectl.md' },
{ 'Agent configuration': 'agent-configuration.md' },
{ 'Contributor relay': 'contributor-relay.md' },
{ 'Troubleshooting': 'troubleshooting.md' },
{ 'Release channels': 'release-channels.md' },
{ 'Backup and disaster recovery': 'backup-dr.md' },
{ 'Running on macOS': 'macos.md' },
]
},
{
title: 'Security',
items: [
{ 'Security model': 'security-model.md' },
{ 'Security threat model': 'security-threat-model.md' },
{ 'ACMM policy matrix': 'acmm-policy-matrix.md' },
]
},
{
title: 'Reference',
items: [
{ 'Architecture Decision Records': [
{ 'ADR index': 'adr/readme.md' },
{ '0001 Record architecture decisions': 'adr/0001-record-architecture-decisions.md' },
{ '0002 MITM proxy network enforcement': 'adr/0002-mitm-proxy-network-enforcement.md' },
{ '0003 ACMM autonomy levels': 'adr/0003-acmm-autonomy-levels.md' },
{ '0004 Beads work ledger': 'adr/0004-beads-work-ledger.md' },
{ '0005 Forge abstraction': 'adr/0005-forge-abstraction.md' },
{ '0006 Planning intelligence': 'adr/0006-planning-intelligence.md' },
{ '0007 Token mint': 'adr/0007-token-mint.md' },
{ '0008 ioscan untrusted input': 'adr/0008-ioscan-untrusted-input.md' },
{ '0009 Trajectory review': 'adr/0009-trajectory-review.md' },
{ '0010 Escalation circuit breaker': 'adr/0010-escalation-circuit-breaker.md' },
{ '0011 Knowledge system': 'adr/0011-knowledge-system.md' },
{ '0012 Skill registry': 'adr/0012-skill-registry.md' },
{ '0013 CEL triggers': 'adr/0013-cel-triggers.md' },
{ '0014 Hub-spoke': 'adr/0014-hub-spoke.md' },
{ '0015 CSP style-src scope': 'adr/0015-csp-style-src-scope.md' },
{ '0016 CSP script-src scope': 'adr/0016-csp-script-src-scope.md' },
{ '0017 Podman Quadlet lifecycle': 'adr/0017-podman-quadlet-lifecycle.md' },
] },
]
}
]

Expand Down Expand Up @@ -186,6 +235,7 @@ const NAV_STRUCTURE_GENERAL: Array<{ title: string; items: NavItem[] }> = [
items: [
{ 'What is Hive Commons?': 'community/what-is-hive-commons.md' },
{ 'Join Hive Commons': 'community/join-hive-commons.md' },
{ 'Community meetings': 'community/meetings.md' },
]
}
]
Expand Down Expand Up @@ -262,10 +312,12 @@ export function buildPageMap(projectId: ProjectId = 'hive') {
meta[title] = title
} else if (allDocFiles.includes(value)) {
processedFiles.add(value)
// const baseName = value.replace(/\.(md|mdx)$/i, '').split('/').pop()!
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
// Use /docs path for general sections, project path for everything else
const isGeneralSection = value.startsWith('contributing/') || value.startsWith('community/') || value.startsWith('news/')
const baseName = value.replace(/\.(md|mdx)$/i, '').split('/').pop()!
const slug = isGeneralSection
? baseName
: title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
const basePathForRoute = isGeneralSection ? 'docs' : projectBasePath
const route = `/${basePathForRoute}/${parentSlug ? parentSlug + '/' : ''}${slug}`
routeMap[`${parentSlug ? parentSlug + '/' : ''}${slug}`] = value
Expand Down
Loading
Loading