Skip to content

Commit 7687b9e

Browse files
committed
Set up experimental builds
The experimental builds are packaged exactly like builds in the stable release channel: same file structure, entry points, and npm package names. The goal is to match what will eventually be released in stable as closely as possible, but with additional features turned on. Versioning and Releasing ------------------------ The experimental builds will be published to the same registry and package names as the stable ones. However, they will be versioned using a separate scheme. Instead of semver versions, experimental releases will receive arbitrary version strings based on their content hashes. The motivation is to thwart attempts to use a version range to match against future experimental releases. The only way to install or depend on an experimental release is to refer to the specific version number. Building -------- I did not use the existing feature flag infra to configure the experimental builds. The reason is because feature flags are designed to configure a single package. They're not designed to generate multiple forks of the same package; for each set of feature flags, you must create a separate package configuration. Instead, I've added a new build dimension called the **release channel**. By default, builds use the **stable** channel. There's also an **experimental** release channel. We have the option to add more in the future. There are now two dimensions per artifact: build type (production, development, or profiling), and release channel (stable or experimental). These are separate dimensions because they are combinatorial: there are stable and experimental production builds, stable and experimental developmenet builds, and so on. You can add something to an experimental build by gating on `__EXPERIMENTAL__`, similar to how we use `__DEV__`. Anything inside these branches will be excluded from the stable builds. This gives us a low effort way to add experimental behavior in any package without setting up feature flags or configuring a new package.
1 parent 75955bf commit 7687b9e

9 files changed

Lines changed: 88 additions & 28 deletions

File tree

.circleci/config.yml

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,18 @@ jobs:
7474
- *run_yarn
7575
- run: yarn test --maxWorkers=2
7676

77+
test_source_experimental:
78+
docker: *docker
79+
environment: *environment
80+
steps:
81+
- checkout
82+
- *restore_yarn_cache
83+
- *run_yarn
84+
- run:
85+
environment:
86+
RELEASE_CHANNEL: experimental
87+
command: yarn test --maxWorkers=2
88+
7789
test_source_persistent:
7890
docker: *docker
7991
environment: *environment
@@ -114,6 +126,30 @@ jobs:
114126
- dist
115127
- sizes/*.json
116128

129+
build_experimental:
130+
docker: *docker
131+
environment: *environment
132+
parallelism: 20
133+
steps:
134+
- checkout
135+
- *restore_yarn_cache
136+
- *run_yarn
137+
- run:
138+
environment:
139+
RELEASE_CHANNEL: experimental
140+
command: |
141+
./scripts/circleci/add_build_info_json.sh
142+
./scripts/circleci/update_package_versions.sh
143+
- run: yarn build
144+
- persist_to_workspace:
145+
root: build
146+
paths:
147+
- facebook-www
148+
- node_modules
149+
- react-native
150+
- dist
151+
- sizes/*.json
152+
117153
process_artifacts:
118154
docker: *docker
119155
environment: *environment
@@ -208,7 +244,7 @@ jobs:
208244

209245
workflows:
210246
version: 2
211-
commit:
247+
stable:
212248
jobs:
213249
- setup
214250
- lint:
@@ -247,9 +283,24 @@ workflows:
247283
- test_dom_fixtures:
248284
requires:
249285
- build
250-
hourly:
286+
287+
experimental:
288+
jobs:
289+
- setup
290+
- test_source_experimental:
291+
requires:
292+
- setup
293+
- build_experimental:
294+
requires:
295+
- setup
296+
- process_artifacts:
297+
requires:
298+
- build_experimental
299+
300+
fuzz_tests:
251301
triggers:
252302
- schedule:
303+
# Fuzz tests run hourly
253304
cron: "0 * * * *"
254305
filters:
255306
branches:

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ module.exports = {
149149
spyOnProd: true,
150150
__PROFILE__: true,
151151
__UMD__: true,
152+
__EXPERIMENTAL__: true,
152153
trustedTypes: true,
153154
},
154155
};

dangerfile.js

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const fetch = require('node-fetch');
3131
const {generateResultsArray} = require('./scripts/rollup/stats');
3232
const {existsSync, readFileSync} = require('fs');
3333
const {exec} = require('child_process');
34+
const {getArtifactsList} = require('./scripts/release/utils');
3435

3536
if (!existsSync('./build/bundle-sizes.json')) {
3637
// This indicates the build failed previously.
@@ -118,13 +119,13 @@ function git(args) {
118119
}
119120

120121
const upstreamRef = danger.github.pr.base.ref;
121-
await git(`remote add upstream https://github.com/facebook/react.git`);
122+
// await git(`remote add upstream https://github.com/facebook/react.git`);
122123
await git('fetch upstream');
123124
const baseCommit = await git(`merge-base HEAD upstream/${upstreamRef}`);
124125

125126
let previousBuildResults = null;
126127
try {
127-
let baseCIBuildId = null;
128+
let baseArtifactsInfo = null;
128129
const statusesResponse = await fetch(
129130
`https://api.github.com/repos/facebook/react/commits/${baseCommit}/status`
130131
);
@@ -133,33 +134,26 @@ function git(args) {
133134
warn(`Base commit is broken: ${baseCommit}`);
134135
return;
135136
}
136-
for (let i = 0; i < statuses.length; i++) {
137+
findArtifactsInfo: for (let i = 0; i < statuses.length; i++) {
137138
const status = statuses[i];
138-
// This must match the name of the CI job that creates the build artifacts
139-
if (status.context === 'ci/circleci: process_artifacts') {
140-
if (status.state === 'success') {
141-
baseCIBuildId = /\/facebook\/react\/([0-9]+)/.exec(
142-
status.target_url
143-
)[1];
144-
break;
145-
}
146-
if (status.state === 'pending') {
147-
warn(`Build job for base commit is still pending: ${baseCommit}`);
148-
return;
149-
}
139+
// CircleCI doesn't have an API to retrieve a workflow ID for a given
140+
// commit, so we have to resort to some trickery. Use the statuses
141+
// endpoint to find a recent status that matches the "stable" workflow.
142+
// It must be a job that doesn't also run in the "experimental" workflow.
143+
if (status.context === 'ci/circleci: build') {
144+
// Scrape the job ID from the url.
145+
const buildJobID = /\/facebook\/react\/([0-9]+)/.exec(
146+
status.target_url
147+
)[1];
148+
baseArtifactsInfo = await getArtifactsList(buildJobID);
150149
}
151150
}
152151

153-
if (baseCIBuildId === null) {
152+
if (baseArtifactsInfo === null) {
154153
warn(`Could not find build artifacts for base commit: ${baseCommit}`);
155154
return;
156155
}
157156

158-
const baseArtifactsInfoResponse = await fetch(
159-
`https://circleci.com/api/v1.1/project/github/facebook/react/${baseCIBuildId}/artifacts`
160-
);
161-
const baseArtifactsInfo = await baseArtifactsInfoResponse.json();
162-
163157
for (let i = 0; i < baseArtifactsInfo.length; i++) {
164158
const info = baseArtifactsInfo[i];
165159
if (info.path === 'home/circleci/project/build/bundle-sizes.json') {

packages/shared/ReactFeatureFlags.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const disableInputAttributeSyncing = false;
5252

5353
// These APIs will no longer be "unstable" in the upcoming 16.7 release,
5454
// Control this behavior with a flag to support 16.6 minor releases in the meanwhile.
55-
export const enableStableConcurrentModeAPIs = false;
55+
export const enableStableConcurrentModeAPIs = __EXPERIMENTAL__;
5656

5757
export const warnAboutShorthandPropertyCollision = false;
5858

scripts/flow/environment.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
declare var __PROFILE__: boolean;
1313
declare var __UMD__: boolean;
14+
declare var __EXPERIMENTAL__: boolean;
1415

1516
declare var __REACT_DEVTOOLS_GLOBAL_HOOK__: any; /*?{
1617
inject: ?((stuff: Object) => void)

scripts/jest/setupEnvironment.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ if (NODE_ENV !== 'development' && NODE_ENV !== 'production') {
77
global.__DEV__ = NODE_ENV === 'development';
88
global.__PROFILE__ = NODE_ENV === 'development';
99
global.__UMD__ = false;
10+
global.__EXPERIMENTAL__ = process.env.RELEASE_CHANNEL === 'experimental';
1011

1112
if (typeof window !== 'undefined') {
1213
global.requestIdleCallback = function(callback) {

scripts/release/prepare-canary-commands/get-latest-master-build-number.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const run = async () => {
1313
entry =>
1414
entry.branch === 'master' &&
1515
entry.status === 'success' &&
16+
entry.workflows.workflow_name === 'build_stable' &&
1617
entry.workflows.job_name === 'process_artifacts'
1718
).build_num;
1819

scripts/release/utils.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,16 @@ const getArtifactsList = async buildID => {
7878
const getBuildInfo = async () => {
7979
const cwd = join(__dirname, '..', '..');
8080

81+
const isExperimental = process.env.RELEASE_CHANNEL === 'experimental';
82+
8183
const branch = await execRead('git branch | grep \\* | cut -d " " -f2', {
8284
cwd,
8385
});
8486
const commit = await execRead('git show -s --format=%h', {cwd});
8587
const checksum = await getChecksumForCurrentRevision(cwd);
86-
const version = `0.0.0-${commit}`;
88+
const version = isExperimental
89+
? `0.0.0-experimental-${commit}`
90+
: `0.0.0-${commit}`;
8791

8892
// Only available for Circle CI builds.
8993
// https://circleci.com/docs/2.0/env-vars/
@@ -94,7 +98,9 @@ const getBuildInfo = async () => {
9498
const packageJSON = await readJson(
9599
join(cwd, 'packages', 'react', 'package.json')
96100
);
97-
const reactVersion = `${packageJSON.version}-canary-${commit}`;
101+
const reactVersion = isExperimental
102+
? `${packageJSON.version}-experimental-canary-${commit}`
103+
: `${packageJSON.version}-canary-${commit}`;
98104

99105
return {branch, buildNumber, checksum, commit, reactVersion, version};
100106
};

scripts/rollup/build.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,8 @@ function getPlugins(
304304
bundleType,
305305
globalName,
306306
moduleType,
307-
pureExternalModules
307+
pureExternalModules,
308+
isExperimentalBuild
308309
) {
309310
const findAndRecordErrorCodes = extractErrorCodes(errorCodeOpts);
310311
const forks = Modules.getForks(bundleType, entry, moduleType);
@@ -362,6 +363,7 @@ function getPlugins(
362363
__PROFILE__: isProfiling || !isProduction ? 'true' : 'false',
363364
__UMD__: isUMDBundle ? 'true' : 'false',
364365
'process.env.NODE_ENV': isProduction ? "'production'" : "'development'",
366+
__EXPERIMENTAL__: isExperimentalBuild,
365367
}),
366368
// We still need CommonJS for external deps like object-assign.
367369
commonjs(),
@@ -485,6 +487,8 @@ async function createBundle(bundle, bundleType) {
485487
module => !importSideEffects[module]
486488
);
487489

490+
const isExperimentalBuild = process.env.RELEASE_CHANNEL === 'experimental';
491+
488492
const rollupConfig = {
489493
input: resolvedEntry,
490494
treeshake: {
@@ -508,7 +512,8 @@ async function createBundle(bundle, bundleType) {
508512
bundleType,
509513
bundle.global,
510514
bundle.moduleType,
511-
pureExternalModules
515+
pureExternalModules,
516+
isExperimentalBuild
512517
),
513518
// We can't use getters in www.
514519
legacy:

0 commit comments

Comments
 (0)