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
2 changes: 2 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,8 @@ jobs:
lockfiles: |
website/pnpm-lock.yaml
- name: Build and test the website
env:
WEBSITE_URL: https://minimax-ai.github.io/OpenAgentCore/
run: make check-website

web-acceptance:
Expand Down
18 changes: 11 additions & 7 deletions .github/workflows/website.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
timeout-minutes: 10
permissions:
contents: read
# configure-pages reads the site's base path.
# The build reads the site's URL from the Pages API.
pages: read
env:
PUBLISH: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }}
Expand All @@ -48,14 +48,18 @@ jobs:
- uses: ./.github/actions/node
with:
lockfiles: website/pnpm-lock.yaml
- name: Read the Pages base path
id: pages
if: env.PUBLISH == 'true'
uses: actions/configure-pages@v6
- name: Build and test
run: make check-website
env:
WEBSITE_BASE: ${{ steps.pages.outputs.base_path }}
GH_TOKEN: ${{ github.token }}
# Exercise the published project path on pull requests.
WEBSITE_URL: https://minimax-ai.github.io/OpenAgentCore/
run: |
if [[ "$PUBLISH" == "true" ]]; then
WEBSITE_URL="$(gh api "repos/$GITHUB_REPOSITORY/pages" --jq .html_url)"
: "${WEBSITE_URL:?The Pages API returned an empty site URL}"
export WEBSITE_URL
fi
make check-website
- name: Upload the site
if: env.PUBLISH == 'true'
uses: actions/upload-pages-artifact@v5
Expand Down
10 changes: 2 additions & 8 deletions website/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { dirname, resolve } from 'node:path'
import { defineConfig, type SiteConfig } from 'vitepress'
import { mermaidFences } from './mermaid.mts'
import { repositoryLinks } from './repository-links.mts'
import { siteBase } from './site-base.mts'
import { docsSidebar, legacyRedirects, pageTitle, readDocsJson, repoRoot } from './docs-nav.mts'

// Pages outside this package resolve Vue from here, not from the repository root.
Expand All @@ -13,14 +14,7 @@ const vueDir = dirname(require.resolve('vue/package.json'))
const repo = 'https://github.com/MiniMax-AI/OpenAgentCore'
const description = 'An open-source, self-hosted implementation of the OpenAI Agents API with multiple native harnesses.'

// GitHub Pages passes its base path (for example `/OpenAgentCore/`, or `/` on a
// custom domain) to the build; local builds serve from the root.
const base = normalizeBase(process.env.WEBSITE_BASE)

function normalizeBase(value: string | undefined): string {
const trimmed = (value ?? '').replace(/^\/+|\/+$/g, '')
return trimmed ? `/${trimmed}/` : '/'
}
const base = siteBase(process.env.WEBSITE_URL)

// The canonical mark, inlined so the favicon has no second copy of the logo.
const logoSvg = readFileSync(resolve(repoRoot, 'docs/assets/openagentcore-logo.svg'), 'utf8')
Expand Down
15 changes: 15 additions & 0 deletions website/.vitepress/site-base.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/** Local builds use the root; published builds derive their path from the Pages URL. */
export function siteBase(value: string | undefined): string {
if (value === undefined) return '/'

let url: URL
try {
url = new URL(value)
} catch {
throw new Error('WEBSITE_URL must be an absolute HTTP(S) URL')
}
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
throw new Error('WEBSITE_URL must be an absolute HTTP(S) URL without credentials, query or fragment')
}
return url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`
}
6 changes: 5 additions & 1 deletion website/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ The landing page lives in `.vitepress/theme/`. `landing-content.ts` holds its En

## Publish

`make check-website` builds and tests the site. core-check runs it for changes under `website/` and to Node dependencies; `.github/workflows/website.yml` runs it for documentation-only pull requests and deploys `main` to GitHub Pages. A repository administrator enables Pages once: **Settings → Pages → Source: GitHub Actions**. The build reads the Pages base path, so the site works both at `https://<owner>.github.io/<repository>/` and on a custom domain set under **Settings → Pages → Custom domain**.
`make check-website` builds and tests the site. core-check runs it for changes under `website/` and to Node dependencies; `.github/workflows/website.yml` runs it for documentation-only pull requests and deploys `main` to GitHub Pages. A repository administrator enables Pages once: **Settings → Pages → Source: GitHub Actions**.

The publishing step reads `html_url` directly from the GitHub Pages API and passes it to the build as `WEBSITE_URL`. VitePress derives the base path from that URL, supporting both `https://<owner>.github.io/<repository>/` and a custom domain set under **Settings → Pages → Custom domain**. An empty or invalid URL stops the build. Local builds omit `WEBSITE_URL` to serve from `/`.

Pull request checks build under the published `/OpenAgentCore/` path. Output tests verify that generated navigation, assets, redirects and `llms.txt` links use the configured path and that local HTML links resolve to generated files. To check a deployment path locally, run `WEBSITE_URL=https://example.com/OpenAgentCore/ make check-website`.
24 changes: 21 additions & 3 deletions website/tests/dist.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
// an HTML page and a raw Markdown copy, legacy paths redirect, both landing
// pages exist and llms.txt lists every page.
import assert from 'node:assert/strict'
import { existsSync, readFileSync } from 'node:fs'
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
import { resolve } from 'node:path'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
import { legacyRedirects, readDocsJson } from '../.vitepress/docs-nav.mts'

const dist = fileURLToPath(new URL('../.vitepress/dist/', import.meta.url))
const pages = readDocsJson().navigation.groups.flatMap((g) => g.pages)
const basePath = new URL(process.env.WEBSITE_URL ?? 'http://localhost/').pathname
const base = basePath.endsWith('/') ? basePath : `${basePath}/`

function html(page) {
return resolve(dist, `${page}.html`)
Expand All @@ -34,16 +36,32 @@ test('every documentation page has HTML and a Markdown copy', () => {
}
})

test('generated navigation and assets stay under the site base and resolve to files', () => {
let checked = 0
for (const file of readdirSync(dist, { recursive: true }).filter((file) => file.endsWith('.html'))) {
const source = readFileSync(resolve(dist, file), 'utf8')
for (const [, href] of source.matchAll(/\b(?:href|src)="(\/[^"\s]*)"/g)) {
assert.ok(href.startsWith(base) && !href.startsWith('//'), `${file}: ${href} must start with ${base}`)
const path = decodeURI(href.split(/[?#]/)[0].slice(base.length))
const candidates = [resolve(dist, path), resolve(dist, `${path}.html`), resolve(dist, path, 'index.html')]
assert.ok(candidates.some((candidate) => existsSync(candidate) && statSync(candidate).isFile()), `${file}: ${href} must resolve to a generated file`)
checked++
}
}
assert.ok(checked > 0, 'generated pages must contain local navigation and assets')
})

test('legacy paths redirect', () => {
for (const { from, to } of legacyRedirects()) {
const source = readFileSync(resolve(dist, `${from}.html`), 'utf8')
assert.ok(source.includes(`url=`) && source.includes(to.replace(/^\//, '')), `${from} → ${to}`)
const target = `${base}${to.replace(/^\//, '')}`
assert.ok(source.includes(`content="0; url=${target}"`), `${from} → ${target}`)
}
})

test('llms.txt lists every page', () => {
const llms = readFileSync(resolve(dist, 'llms.txt'), 'utf8')
for (const page of pages) assert.ok(llms.includes(`${page}.md)`), `${page} in llms.txt`)
for (const page of pages) assert.ok(llms.includes(`](${base}${page}.md)`), `${page} in llms.txt uses ${base}`)
})

test('Mermaid fences become diagrams, not code blocks', () => {
Expand Down
23 changes: 23 additions & 0 deletions website/tests/site-base.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { siteBase } from '../.vitepress/site-base.mts'

test('local builds serve from the root', () => {
assert.equal(siteBase(undefined), '/')
})

test('Pages URLs support project paths, organization sites and custom domains', () => {
for (const [url, expected] of [
['http://minimax-ai.github.io/OpenAgentCore/', '/OpenAgentCore/'],
['https://octocat.github.io/my-repo', '/my-repo/'],
['https://octocat.github.io/', '/'],
['https://docs.example.com', '/'],
['https://docs.example.com/nested/site/', '/nested/site/'],
]) assert.equal(siteBase(url), expected, url)
})

test('empty or invalid Pages metadata fails instead of building at the root', () => {
for (const value of ['', ' ', 'null', '/OpenAgentCore/', 'file:///tmp/site', 'https://user:pass@example.com/', 'https://example.com/?path=docs', 'https://example.com/#docs']) {
assert.throws(() => siteBase(value), /WEBSITE_URL/, JSON.stringify(value))
}
})
Loading