Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add docs versioning #310

Merged
merged 14 commits into from
Nov 11, 2021
10 changes: 9 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: 2
jobs:
build:
docker:
@@ -27,3 +26,12 @@ jobs:
- run: GATSBY_CPU_COUNT=1 GATSBY_SKIP_ADDON_PAGES=true yarn build
- run: yarn build-storybook
- run: yarn chromatic --storybook-build-dir storybook-static --exit-zero-on-changes

workflows:
version: 2
build:
jobs:
- build:
filters:
branches:
ignore: \^release-[0-9]+-[0-9]+$\
13 changes: 13 additions & 0 deletions .github/workflows/push-all-release-branches.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Push all release branches

on:
push:
branches:
- master

jobs:
push-branches:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: ./scripts/push-all-release-branches.sh
14 changes: 14 additions & 0 deletions .github/workflows/respond-to-monorepo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Respond to monorepo

on: [repository_dispatch]

jobs:
build:
runs-on: ubuntu-latest
if: github.event.action == 'request-create-frontpage-branch'
steps:
- uses: actions/checkout@v2
- run: |
echo 'Creating branch ${{ github.event.client_payload.branch }}...'
git branch temp
git push -f origin temp:${{ github.event.client_payload.branch }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ monorepo

# Pulled from monorepo
src/content/docs
src/generated/versions

# Env vars
.env.production
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -58,6 +58,10 @@ Release notes are stored in the src/content/releases directory as `.md` files. T

Within the release's `.md` file, frontmatter is used to create a page title, while the rest of the content is parsed using `gatsby-transformer-remark` and styled with selectors in `src/styles/formatting.js`.

### Publishing new versions

[See detailed docs](docs/versioning.md)

### Search

Search within the docs is powered by [DocSearch](https://docsearch.algolia.com/). In order for this to work, an environment variable is required:
45 changes: 45 additions & 0 deletions docs/versioning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Docs versioning

This site is configured to build its doc pages from a variable version of the content in the [Storybook monorepo](https://github.com/storybookjs/storybook). This is mostly automated.

> In this document, assume "latest" is `6.3` and "next" is `6.4`.
>
> "monorepo release branch" = `main`, `next`, and `release-x-x` (starting with `release-6-0`).
> "frontpage release branch" = `master` and `release-x-x` (starting with `release-6-0`).
## Publishing new versions

When a pre-release ("next") version graduates to stable (and a new "next" version is cut):

_First, in the monorepo:_

1. Create a release branch from `main`.
- `release-6-3`, in this document.
1. Make sure any release branch has an appropriate version in its root `package.json`.

_Second, in this repo:_

1. Make sure each release branch in the monorepo has a corresponding [release note](../README.md#release-notes), and that their contents are correct.
1. Add the version that _was_ "latest" to the [Netlify branch deploy setting](https://app.netlify.com/sites/storybook-frontpage/settings/deploys).
- `release-6-3`, in this document.
1. Push any updates to `master`.

## How it works

1. Pushing to a monorepo release branch triggers a [workflow](https://github.com/storybookjs/storybook/tree/next/.github/workflows/handle-release-branches.yml):
- On push to `main`
- Use webhook to kick off production frontpage deploy
- On push to `next`
- Creates & force-pushes `release-6-4` branch
- Sends [dispatch event](https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#repository_dispatch) to this repo which kicks off a [workflow](../.github/workflows/respond-to-monorepo.yml) to create and force-push `release-6-4` branch
- On push to `release-x-x`
- If pushing to `release-6-4`
- Warns that changes will be lost on next push to `next`
- Else
- Sends dispatch event to this repo which kicks off a workflow to create and force-push `release-x-x` branch
1. Pushing those `release-x-x` in this repo will kick off a [Netlify branch deploy](https://docs.netlify.com/site-deploys/overview/#branches-and-deploys) for the appropriate version.
1. When the docs content is extracted from the monorepo, each of the other version's info is extracted as well (for generating the list of available versions)
1. Based on the latest and current version info, the site adjusts the URLs and other details appropriately. ([See the PR for details](https://github.com/storybookjs/frontpage/pull/310).)
1. Using [Netlify proxy rewrites](https://docs.netlify.com/routing/redirects/rewrites-proxies/#proxy-to-another-netlify-site), the production site and branch deploys are stitched together to appear as a single site.
1. Pushing to `master` in this repo kicks off a [workflow](../.github/workflows/push-all-release-branches.yml) which will create new release branches from `master` and push them, kicking off branch deploys for each.
- This ensures all branch deploys stay in-sync with any updates to production
55 changes: 36 additions & 19 deletions gatsby-config.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
const path = require('path');
const { global } = require('@storybook/design-system');
const siteMetadata = require('./site-metadata');
const getReleaseBranchUrl = require('./src/util/get-release-branch-url');

require('dotenv').config({
path: `.env.${process.env.NODE_ENV}`,
});

const { isLatest, versionString } = siteMetadata;

module.exports = {
siteMetadata,
flags: {
PRESERVE_WEBPACK_CACHE: true,
FAST_DEV: true,
QUERY_ON_DEMAND: true,
},
...(!isLatest ? { assetPrefix: getReleaseBranchUrl(versionString) } : undefined),
plugins: [
'gatsby-plugin-react-helmet',
'gatsby-plugin-typescript',
@@ -131,11 +135,18 @@ module.exports = {
headers: {
// Remove `X-Frame-Options: DENY` default header so that the release notes can
// be served in an iframe.
'/*': ['X-XSS-Protection: 1; mode=block', 'X-Content-Type-Options: nosniff'],
'/*': [
'X-XSS-Protection: 1; mode=block',
'X-Content-Type-Options: nosniff',
...(!isLatest ? ['Access-Control-Allow-Origin: *'] : []),
],
'/versions.json': [
'Access-Control-Allow-Origin: *',
'Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept',
],
...(!isLatest && {
[`/docs/${versionString}/*`]: ['X-Robots-Tag: noindex'],
}),
},
// Do not use the default security headers. Use those we have defined above.
mergeSecurityHeaders: false,
@@ -148,24 +159,30 @@ module.exports = {
component: require.resolve('./src/components/layout/PageLayout'),
},
},
{
resolve: `gatsby-plugin-sitemap`,
options: {
serialize: ({ site, allSitePage }) => {
const allPages = allSitePage.edges.map((edge) => edge.node);
...(isLatest
? [
{
resolve: `gatsby-plugin-sitemap`,
options: {
serialize: ({ site, allSitePage }) => {
const allPages = allSitePage.edges.map((edge) => edge.node);

return allPages.map((page) => {
return {
url: `${site.siteMetadata.siteUrl}${page.path}/`,
changefreq: `daily`,
priority: 0.7,
};
});
},
// Exclude all doc pages not for React
// except the get-started/introduction page for all frameworks
exclude: ['{/docs/!(react)/!(get-started)/**,/docs/!(react)/get-started/!(introduction)}'],
},
},
return allPages.map((page) => {
return {
url: `${site.siteMetadata.siteUrl}${page.path}/`,
changefreq: `daily`,
priority: 0.7,
};
});
},
// Exclude all doc pages not for React
// except the get-started/introduction page for all frameworks
exclude: [
'{/docs/!(react)/!(get-started)/**,/docs/!(react)/get-started/!(introduction)}',
],
},
},
]
: []),
],
};
109 changes: 86 additions & 23 deletions gatsby-node.js
Original file line number Diff line number Diff line change
@@ -3,26 +3,66 @@ const path = require('path');

const { createFilePath } = require(`gatsby-source-filesystem`);

const { versionString, latestVersion, latestVersionString, isLatest } = require('./site-metadata');
const { toc: docsToc } = require('./src/content/docs/toc');
const addStateToToc = require('./src/util/add-state-to-toc');
const buildPathWithFramework = require('./src/util/build-path-with-framework');
const createAddonsPages = require('./src/util/create-addons-pages');
const getReleaseBranchUrl = require('./src/util/get-release-branch-url');

const githubDocsBaseUrl = 'https://github.com/storybookjs/storybook/tree/next';
const addStateToToc = (items, pathPrefix = '/docs') =>
items.map((item) => {
const itemPath = item.pathSegment ? `${pathPrefix}/${item.pathSegment}` : pathPrefix;
const VERSION_PARTS_REGEX = /^(\d+\.\d+)(?:\.\d+)?-?(\w+)?(?:\.\d+$)?/;

const docsTocWithPaths = addStateToToc(docsToc);

/* Creates a structure like:
*
* {
* "stable": [
* { "version": 6.3, "string": "6.3", "label": "latest" },
* { "version": 6.2, "string": "6.2" },
* { "version": 6.1, "string": "6.1" },
* { "version": 6, "string": "6.0" }
* ],
* "preRelease": [
* { "version": 6.4, "string": "6.4", "label": "beta" },
* { "version": 7, "string": "7.0", "label": "alpha" }
* ]
* }
*
* Note that the stable releases are sorted in descending order, and pre-releases in ascending.
*
* The simple sorting logic will break if the minor version number is >= 10, e.g. 6.10.
* That seems very unlikely, given our release cadence, so we opted to keep it simple.
*/
const versions = fs
.readdirSync('./src/generated/versions')
.filter((v) => v.match(VERSION_PARTS_REGEX))
.sort((a, b) => parseFloat(b || latestVersion) - parseFloat(a || latestVersion))
.map((v) => {
// eslint-disable-next-line global-require, import/no-dynamic-require
const { version: versionFromFile } = require(`./src/generated/versions/${v}/package.json`);
const [, string, label] = versionFromFile.match(VERSION_PARTS_REGEX);
return {
...item,
...(item.type.match(/link/) && {
path: itemPath,
githubUrl: `${githubDocsBaseUrl}${itemPath}.md`,
}),
...(item.children && { children: addStateToToc(item.children, itemPath) }),
version: Number(string),
string,
label,
};
});

const docsTocWithPaths = addStateToToc(docsToc);
})
.reduce(
(acc, v) => {
if (v.version > latestVersion) {
acc.preRelease.unshift(v);
} else {
acc.stable.push(v);
}
return acc;
},
{
stable: [{ version: latestVersion, label: 'latest', string: latestVersionString }],
preRelease: [],
}
);
const nextVersionString = versions.preRelease[0].string;

exports.onCreateNode = ({ actions, getNode, node }) => {
const { createNodeField } = actions;
@@ -42,9 +82,9 @@ exports.onCreateNode = ({ actions, getNode, node }) => {
createNodeField({ node, name: 'slug', value: slug });

if (pageType === 'releases') {
const [_, version] = slugParts;
createNodeField({ node, name: 'iframeSlug', value: `/releases/iframe/${version}/` });
createNodeField({ node, name: 'version', value: version });
const [_, releaseVersion] = slugParts;
createNodeField({ node, name: 'iframeSlug', value: `/releases/iframe/${releaseVersion}/` });
createNodeField({ node, name: 'version', value: releaseVersion });
}
}
};
@@ -118,9 +158,9 @@ exports.createPages = ({ actions, graphql }) => {
);
let latestRelease;
sortedReleases.forEach(({ node }) => {
const { pageType, iframeSlug, slug, version } = node.fields;
const { pageType, iframeSlug, slug, version: releaseVersion } = node.fields;
// Data passed to context is available in page queries as GraphQL variables.
const context = { pageType, slug, version };
const context = { pageType, slug, version: releaseVersion };

createPage({
path: slug,
@@ -162,7 +202,10 @@ exports.createPages = ({ actions, graphql }) => {
const docsTocByFramework = Object.fromEntries(
frameworks.map((framework) => [
framework,
addStateToToc(docsTocWithPaths, `/docs/${framework}`),
addStateToToc(
docsTocWithPaths,
`/docs/${isLatest ? framework : `${versionString}/${framework}`}`
),
])
);
const createDocsPages = (tocItems) => {
@@ -177,13 +220,16 @@ exports.createPages = ({ actions, graphql }) => {
const nextTocItem = tocItems[index + 1];

frameworks.forEach((framework) => {
const fullPath = buildPathWithFramework(slug, framework);
createPage({
path: buildPathWithFramework(slug, framework),
path: fullPath,
component: path.resolve(`./src/components/screens/DocsScreen/DocsScreen.tsx`),
context: {
pageType,
layout: 'docs',
slug,
fullPath,
versions,
framework,
docsToc: docsTocByFramework[framework],
tocItem,
@@ -213,7 +259,7 @@ exports.createPages = ({ actions, graphql }) => {

if (firstDocsPageSlug) {
createRedirect({
fromPath: `/docs/`,
fromPath: `/docs/${isLatest ? '' : versionString}`,
isPermanent: false,
redirectInBrowser: true,
toPath: buildPathWithFramework(firstDocsPageSlug, frameworks[0]),
@@ -222,7 +268,7 @@ exports.createPages = ({ actions, graphql }) => {
// Setup a redirect for each framework to the first guide
frameworks.forEach((framework) => {
createRedirect({
fromPath: `/docs/${framework}`,
fromPath: `/docs/${isLatest ? framework : `${versionString}/${framework}`}`,
isPermanent: false,
redirectInBrowser: true,
toPath: buildPathWithFramework(firstDocsPageSlug, framework),
@@ -232,7 +278,7 @@ exports.createPages = ({ actions, graphql }) => {
}
)
.then(() => {
return process.env.GATSBY_SKIP_ADDON_PAGES
return process.env.GATSBY_SKIP_ADDON_PAGES || !isLatest
? Promise.resolve()
: createAddonsPages({ actions, graphql });
})
@@ -260,6 +306,23 @@ function generateVersionsFile() {
fs.writeFileSync('./public/versions-raw.json', JSON.stringify(data));
}

function updateRedirectsFile() {
const originalContents = fs.readFileSync('./static/_redirects');
const newContents = [...versions.stable, ...versions.preRelease]
.reduce((acc, { string }) => {
if (string !== latestVersionString) {
acc.push(`/docs/${string}/* ${getReleaseBranchUrl(string)}/docs/${string}/:splat 200`);
}
return acc;
}, [])
.concat([
`/docs/next/* ${getReleaseBranchUrl(nextVersionString)}/docs/${nextVersionString}/:splat 200`,
])
.join('\n');
fs.writeFileSync('./public/_redirects', `${originalContents}\n\n${newContents}`);
}

exports.onPostBuild = () => {
generateVersionsFile();
updateRedirectsFile();
};
2 changes: 1 addition & 1 deletion netlify.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[build]
command = "yarn build:all"
command = "yarn build:with-prefix"
functions = "/functions"
Loading