-
Notifications
You must be signed in to change notification settings - Fork 60k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #19636 from github/feature-versioning
Feature-based versioning
- Loading branch information
Showing
12 changed files
with
363 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
## Feature-based versioning | ||
|
||
Feature-based versioning allows us to define and control the versions of an arbitrarily named "feature" in one place. | ||
|
||
**Note**: Do not delete `data/features/placeholder.yml` because it is used by tests. | ||
|
||
## How it works | ||
|
||
Add a new YAML file with the feature name you want to use in this directory. For a feature named `meow`, that would be `data/features/meow.yml`. | ||
|
||
Add a `versions` block to the YML file with the short names of the versions the feature is available in. For example: | ||
|
||
```yaml | ||
versions: | ||
fpt: '*' | ||
ghes: '>3.1' | ||
ghae: '*' | ||
``` | ||
The format and allowed values are the same as the [frontmatter versions property](/content#versions). | ||
### Liquid conditionals | ||
Now you can use `{% if meow %} ... {% endif %}` in content files! Note this is the `if` tag, not the new `ifversion` tag. | ||
|
||
### Frontmatter | ||
|
||
You can also use the feature in frontmatter in content files: | ||
|
||
```yaml | ||
versions: | ||
fpt: '*' | ||
ghes: '>3.1' | ||
feature: 'meow' | ||
``` | ||
|
||
If you want a content file to apply to more than one feature, you can do this: | ||
|
||
```yaml | ||
versions: | ||
fpt: '*' | ||
ghes: '>3.1' | ||
feature: ['meow', 'blorp'] | ||
``` | ||
|
||
## Schema enforcement | ||
|
||
The schema for validating the feature versioning lives in [`tests/helpers/schemas/feature-versions.js`](tests/helpers/schemas/feature-versions.js) and is exercised by [`tests/content/lint-files.js`](tests/content/lint-files.js). | ||
|
||
## Script to remove feature tags | ||
|
||
TBD! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Do not delete! Used by tests. | ||
versions: | ||
ghes: '>3.0' | ||
ghae: '*' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,17 @@ | ||
const path = require('path') | ||
const { reduce, sortBy } = require('lodash') | ||
const allVersions = require('./all-versions') | ||
const versionSatisfiesRange = require('./version-satisfies-range') | ||
const checkIfNextVersionOnly = require('./check-if-next-version-only') | ||
const dataDirectory = require('./data-directory') | ||
const encodeBracketedParentheses = require('./encode-bracketed-parentheses') | ||
const featuresDir = path.posix.join(__dirname, '../data/features') | ||
|
||
const featureData = dataDirectory(featuresDir, { | ||
preprocess: dataString => | ||
encodeBracketedParentheses(dataString.trimEnd()), | ||
ignorePatterns: [/README\.md$/] | ||
}) | ||
|
||
// return an array of versions that an article's product versions encompasses | ||
function getApplicableVersions (frontmatterVersions, filepath) { | ||
|
@@ -13,17 +24,63 @@ function getApplicableVersions (frontmatterVersions, filepath) { | |
return Object.keys(allVersions) | ||
} | ||
|
||
// get an array like: [ 'free-pro-team@latest', '[email protected]', 'enterprise-cloud@latest' ] | ||
const applicableVersions = [] | ||
// Check for frontmatter that includes a feature name, like: | ||
// fpt: '*' | ||
// feature: 'foo' | ||
// or multiple feature names, like: | ||
// fpt: '*' | ||
// feature: ['foo', 'bar'] | ||
// and add the versions affiliated with the feature (e.g., foo) to the frontmatter versions object: | ||
// fpt: '*' | ||
// ghes: '>=2.23' | ||
// ghae: '*' | ||
// where the feature is bringing the ghes and ghae versions into the mix. | ||
const featureVersions = reduce(frontmatterVersions, (result, value, key) => { | ||
if (key === 'feature') { | ||
if (typeof value === 'string') { | ||
Object.assign(result, { ...featureData[value].versions }) | ||
} else if (Array.isArray(value)) { | ||
value.forEach(str => { | ||
Object.assign(result, { ...featureData[str].versions }) | ||
}) | ||
} | ||
delete result[key] | ||
} | ||
return result | ||
}, {}) | ||
|
||
// We will be evaluating feature versions separately, so we can remove this. | ||
delete frontmatterVersions.feature | ||
|
||
// Get available versions for frontmatter and for feature versions. | ||
const foundFeatureVersions = evaluateVersions(featureVersions) | ||
const foundFrontmatterVersions = evaluateVersions(frontmatterVersions) | ||
|
||
// Combine them! | ||
const applicableVersions = [...new Set(foundFrontmatterVersions.versions.concat(foundFeatureVersions.versions))] | ||
|
||
if (!applicableVersions.length && !foundFrontmatterVersions.isNextVersionOnly && !foundFeatureVersions.isNextVersionOnly) { | ||
throw new Error(`No applicable versions found for ${filepath}. Please double-check the page's \`versions\` frontmatter.`) | ||
} | ||
|
||
// Sort them by the order in lib/all-versions. | ||
const sortedVersions = sortBy(applicableVersions, (v) => { return Object.keys(allVersions).indexOf(v) }) | ||
|
||
return sortedVersions | ||
} | ||
|
||
function evaluateVersions (versionsObj) { | ||
let isNextVersionOnly = false | ||
|
||
// where frontmatter is something like: | ||
// get an array like: [ 'free-pro-team@latest', '[email protected]', 'enterprise-cloud@latest' ] | ||
const versions = [] | ||
|
||
// where versions obj is something like: | ||
// fpt: '*' | ||
// ghes: '>=2.19' | ||
// ghae: '*' | ||
// ^ where each key corresponds to a plan's short name (defined in lib/all-versions.js) | ||
Object.entries(frontmatterVersions) | ||
Object.entries(versionsObj) | ||
.forEach(([plan, planValue]) => { | ||
// Special handling for frontmatter that evalues to the next GHES release number or a hardcoded `next`. | ||
isNextVersionOnly = checkIfNextVersionOnly(planValue) | ||
|
@@ -37,16 +94,12 @@ function getApplicableVersions (frontmatterVersions, filepath) { | |
const versionToCompare = relevantVersion.hasNumberedReleases ? relevantVersion.currentRelease : '1.0' | ||
|
||
if (versionSatisfiesRange(versionToCompare, planValue)) { | ||
applicableVersions.push(relevantVersion.version) | ||
versions.push(relevantVersion.version) | ||
} | ||
}) | ||
}) | ||
|
||
if (!applicableVersions.length && !isNextVersionOnly) { | ||
throw new Error(`No applicable versions found for ${filepath}. Please double-check the page's \`versions\` frontmatter.`) | ||
} | ||
|
||
return applicableVersions | ||
return { versions, isNextVersionOnly } | ||
} | ||
|
||
module.exports = getApplicableVersions |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
const getApplicableVersions = require('../../lib/get-applicable-versions') | ||
|
||
module.exports = async function features (req, res, next) { | ||
if (!req.context.page) return next() | ||
|
||
// Determine whether the currentVersion belongs to the list of versions the feature is available in. | ||
Object.keys(req.context.site.data.features).forEach(featureName => { | ||
const { versions } = req.context.site.data.features[featureName] | ||
const applicableVersions = getApplicableVersions(versions, req.path) | ||
|
||
// Adding the resulting boolean to the context object gives us the ability to use | ||
// `{% if featureName ... %}` conditionals in content files. | ||
const isFeatureAvailableInCurrentVersion = applicableVersions.includes(req.context.currentVersion) | ||
req.context[featureName] = isFeatureAvailableInCurrentVersion | ||
}) | ||
|
||
return next() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
title: Some article only versioned for FPT | ||
versions: | ||
fpt: '*' | ||
ghes: '>2.21' | ||
feature: 'placeholder' | ||
--- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
const { schema } = require('../../../lib/frontmatter') | ||
|
||
// Copy the properties from the frontmatter schema. | ||
const featureVersions = { | ||
properties: { | ||
versions: Object.assign({}, schema.properties.versions) | ||
} | ||
} | ||
|
||
// Remove the feature versions properties. | ||
// We don't want to allow features within features! We just want pure versioning. | ||
delete featureVersions.properties.versions.properties.feature | ||
|
||
// Call it invalid if any properties other than version properties are found. | ||
featureVersions.additionalProperties = false | ||
featureVersions.properties.versions.additionalProperties = false | ||
|
||
module.exports = featureVersions |
Oops, something went wrong.