Skip to content
Open
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
20 changes: 16 additions & 4 deletions .agents/skills/publishing-docs-versions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,19 @@ Use this skill for the Sourcegraph docs repo release-version workflow: cutting l
- `docs.config.js`
- `src/data/versions.ts`
- `docs/legacy.mdx`
- `src/data/versions.ts` on `origin/main` is the canonical dropdown list. The
current site exposes it through `/docs/api/versions`, and legacy selectors
load that manifest at runtime.
- A legacy branch's bundled `src/data/versions.ts` is only a fallback. Its first
entry identifies the archived site and must not have the `latest` label.

## Standard workflow for a new release

For a new release `X.Y`:

1. Archive the previous docs version `P.Q` in the `legacy` remote.
2. Ensure the legacy branch’s own config says it is version `P.Q` and lists older versions only.
2. Ensure the legacy branch’s own config says it is version `P.Q`, marks it as
selected rather than latest, and lists older fallback versions only.
3. Update `origin` so `X.Y` is latest and `P.Q` appears as a previous version.

Example: when 7.4 is released, archive 7.3 as `legacy/v7_3`, then update `origin` with `DOCS_LATEST_VERSION: '7.4'` and add 7.3 to previous-version lists.
Expand Down Expand Up @@ -63,12 +69,15 @@ git commit --allow-empty -m "Branch for docs version X.Y"
git push -u legacy vX_Y
```

Then update the legacy branch so it identifies itself as `X.Y` and lists only older versions.
Then update the legacy branch so it identifies itself as `X.Y` and lists only
older fallback versions. The runtime manifest supplies the current canonical
list when the current docs site is available.

For `v7_3`, for example:

- `docs.config.js`: `DOCS_LATEST_VERSION: '7.3'`
- `src/data/versions.ts`: first entry remains `latest`; previous entries should include `v7.2`, `v7.1`, `v7.0`, then 6.x.
- `src/data/versions.ts`: first entry identifies `v7.3` without a `latest`
label; fallback entries should include `v7.2`, `v7.1`, `v7.0`, then 6.x.
- `docs/legacy.mdx`: `Sourcegraph 7.X` should include `7.2`, `7.1`, `7.0` (not 7.3 itself).

Commit and push:
Expand Down Expand Up @@ -125,6 +134,9 @@ git ls-remote --heads legacy 'v7_*'
Confirm:

- The legacy branch points to the pushed commit.
- The legacy branch lists only older previous versions.
- The legacy branch identifies itself without claiming to be latest and lists
only older fallback versions.
- The origin PR branch sets the new latest version and includes the archived version in previous-version lists.
- The current manifest lists the new latest version first, and its URL points to
`https://sourcegraph.com/docs`.
- Return the local workspace to clean `main` unless the user asked to stay on a release branch.
19 changes: 19 additions & 0 deletions src/app/api/versions/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {versions} from '@/data/versions';
import {NextResponse} from 'next/server';

export function GET() {
return NextResponse.json(
versions.map((version, index) =>
index === 0
? {...version, url: 'https://sourcegraph.com/docs'}
: version
),
{
headers: {
'Access-Control-Allow-Origin': '*',
'Cache-Control':
'public, max-age=0, s-maxage=300, stale-while-revalidate=60'
}
}
);
}
111 changes: 83 additions & 28 deletions src/components/VersionSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,78 @@

import {VersionI, versions} from '@/data/versions';
import {Menu, Transition} from '@headlessui/react';
import {ArrowUpRightIcon, ChevronDownIcon} from '@heroicons/react/20/solid';
import {
ArrowUpRightIcon,
CheckIcon,
ChevronDownIcon
} from '@heroicons/react/20/solid';
import clsx from 'clsx';
import Link from 'next/link';
import {usePathname} from 'next/navigation';
import {Fragment, useEffect, useState} from 'react';

const versionsUrl =
process.env.NEXT_PUBLIC_DOCS_VERSIONS_URL ??
'https://sourcegraph.com/docs/api/versions';

function isVersion(value: unknown): value is VersionI {
if (typeof value !== 'object' || value === null) return false;

const version = value as Partial<VersionI>;
return (
typeof version.name === 'string' &&
typeof version.url === 'string' &&
(version.label === undefined || typeof version.label === 'string')
);
}

export default function VersionSelector() {
const path = usePathname();
const [availableVersions, setAvailableVersions] =
useState<VersionI[]>(versions);

const [selectedVersion, setSelectedVersion] = useState<VersionI>(
versions[0]
);
const segments = path.split('/');
const versionIndex = segments.findIndex(segment => segment === 'v');
const versionName = versionIndex >= 0 && segments[versionIndex + 1];
const selectedVersionName = versionName
? `v${versionName}`
: versions[0].name;
const selectedVersion =
availableVersions.find(
version => version.name === selectedVersionName
) ?? versions[0];

useEffect(() => {
// Extract the version name from the URL path, if any
const segments = path.split('/');
const versionIndex = segments.findIndex(segment => segment === 'v');
// Versioned link example:
// docs/v/5.1.2/ where versionName = 5.1.2
const versionName = versionIndex >= 0 && segments[versionIndex + 1];
if (!versionName) {
setSelectedVersion(versions[0]);
return;
}
const controller = new AbortController();

void fetch(versionsUrl, {signal: controller.signal})
.then(response => (response.ok ? response.json() : null))
.then((remoteVersions: unknown) => {
if (
!Array.isArray(remoteVersions) ||
remoteVersions.length === 0 ||
!remoteVersions.every(isVersion)
) {
return;
}

setAvailableVersions(
remoteVersions.some(
version => version.name === versions[0].name
)
? remoteVersions
: [
...remoteVersions,
{...versions[0], label: undefined}
]
);
})
.catch(() => {
// Keep this build's version list if the current site is unavailable.
});

// If version exists, select it
const matchedVersion = versions.find(version =>
version.url.includes(versionName)
);
if (matchedVersion) setSelectedVersion(matchedVersion);
}, [path]);
return () => controller.abort();
}, []);

return (
<Menu as="div" className="relative inline-block text-left">
Expand All @@ -41,7 +82,9 @@ export default function VersionSelector() {
className="inline-flex w-full items-center justify-center gap-x-1.5
rounded-md px-2 py-2 text-xs font-medium text-slate-500 shadow-sm ring-1 ring-inset ring-light-border-2 hover:bg-vermilion-08 hover:text-vermilion-11 dark:bg-dark-bg-2 dark:text-dark-paragraph-text dark:ring-inset dark:ring-dark-border"
>
{selectedVersion.label === 'latest' ? 'Latest' : selectedVersion.name}
{selectedVersion.label === 'latest'
? 'Latest'
: selectedVersion.name}
<ChevronDownIcon
className="-mr-1 h-4 w-4"
aria-hidden="true"
Expand All @@ -58,17 +101,20 @@ export default function VersionSelector() {
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="divide- absolute right-0 z-10 mt-2 flex w-32 origin-top-right flex-col divide-light-border rounded-md bg-light-bg-1 text-xs text-slate-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:w-48 sm:text-sm dark:divide-dark-border dark:bg-dark-bg-2 dark:text-dark-paragraph-text">
<Menu.Items className="divide- absolute right-0 z-10 mt-2 flex max-h-[calc(100vh-12rem)] w-32 origin-top-right flex-col divide-light-border overflow-y-auto overscroll-contain rounded-md bg-light-bg-1 text-xs text-slate-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:divide-dark-border dark:bg-dark-bg-2 dark:text-dark-paragraph-text sm:w-48 sm:text-sm">
{/* Versions */}
{versions.length > 0 &&
versions.map((version, count) => (
{availableVersions.length > 0 &&
availableVersions.map((version, count) => (
<Menu.Item key={version.name}>
{({active}) => (
<a
href={version.url}
target="_blank"
onClick={() =>
setSelectedVersion(version)
aria-current={
version.name ===
selectedVersion.name
? 'page'
: undefined
}
className={clsx(
'hover:bg-vermilion-08 hover:text-vermilion-11',
Expand All @@ -79,11 +125,20 @@ export default function VersionSelector() {
)}
>
{version.name}
{version.label && (
{version.label ? (
<span className="font-medium text-link">
{version.label}
</span>
)}
) : version.name ===
selectedVersion.name ? (
<span className="flex items-center gap-0.5 rounded bg-slate-700 px-1.5 py-0.5 text-[10px] font-medium text-white dark:bg-slate-200 dark:text-slate-900">
<CheckIcon
className="h-3 w-3"
aria-hidden="true"
/>
selected
</span>
) : null}
</a>
)}
</Menu.Item>
Expand Down