Skip to content

Commit

Permalink
Set up experimental builds
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite committed Oct 12, 2019
1 parent ab3ae7d commit 8c4854d
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 26 deletions.
55 changes: 53 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ jobs:
- *run_yarn
- run: yarn test --maxWorkers=2

test_source_experimental:
docker: *docker
environment: *environment
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- run:
environment:
RELEASE_CHANNEL: experimental
command: yarn test --maxWorkers=2

test_source_persistent:
docker: *docker
environment: *environment
Expand Down Expand Up @@ -114,6 +126,30 @@ jobs:
- dist
- sizes/*.json

build_experimental:
docker: *docker
environment: *environment
parallelism: 20
steps:
- checkout
- *restore_yarn_cache
- *run_yarn
- run:
environment:
RELEASE_CHANNEL: experimental
command: |
./scripts/circleci/add_build_info_json.sh
./scripts/circleci/update_package_versions.sh
- run: yarn build
- persist_to_workspace:
root: build
paths:
- facebook-www
- node_modules
- react-native
- dist
- sizes/*.json

process_artifacts:
docker: *docker
environment: *environment
Expand Down Expand Up @@ -208,7 +244,7 @@ jobs:

workflows:
version: 2
commit:
stable:
jobs:
- setup
- lint:
Expand Down Expand Up @@ -247,9 +283,24 @@ workflows:
- test_dom_fixtures:
requires:
- build
hourly:

experimental:
jobs:
- setup
- test_source_experimental:
requires:
- setup
- build_experimental:
requires:
- setup
- process_artifacts:
requires:
- build_experimental

fuzz_tests:
triggers:
- schedule:
# Fuzz tests run hourly
cron: "0 * * * *"
filters:
branches:
Expand Down
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ module.exports = {
spyOnProd: true,
__PROFILE__: true,
__UMD__: true,
__EXPERIMENTAL__: true,
trustedTypes: true,
},
};
59 changes: 40 additions & 19 deletions dangerfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ function git(args) {

let previousBuildResults = null;
try {
let baseCIBuildId = null;
let baseArtifactsInfo = null;
const statusesResponse = await fetch(
`https://api.github.com/repos/facebook/react/commits/${baseCommit}/status`
);
Expand All @@ -133,33 +133,54 @@ function git(args) {
warn(`Base commit is broken: ${baseCommit}`);
return;
}
for (let i = 0; i < statuses.length; i++) {
findArtifactsInfo: for (let i = 0; i < statuses.length; i++) {
const status = statuses[i];
// This must match the name of the CI job that creates the build artifacts
if (status.context === 'ci/circleci: process_artifacts') {
if (status.state === 'success') {
baseCIBuildId = /\/facebook\/react\/([0-9]+)/.exec(
status.target_url
)[1];
break;
}
if (status.state === 'pending') {
warn(`Build job for base commit is still pending: ${baseCommit}`);
return;
// CircleCI doesn't have an API to retrieve a workflow ID for a given
// commit, so we have to resort to some trickery. Use the statuses
// endpoint to find a recent status that matches the "stable" workflow.
// It must be a job that doesn't also run in the "experimental" workflow.
if (status.context === 'ci/circleci: build') {
// Scrape the job ID from the url.
const buildJobID = /\/facebook\/react\/([0-9]+)/.exec(
status.target_url
)[1];

// Get the workflow that this job belongs to
const buildJobMetadataResponse = await fetch(
`https://circleci.com/api/v1.1/project/gh/facebook/react/${buildJobID}`
);
const buildJobMetadata = await buildJobMetadataResponse.json();
const workflowID = buildJobMetadata.workflows.workflow_id;

// Now we can get the jobs that are part of this workflow.
const jobsResponse = await fetch(
`https://circleci.com/api/v2/workflow/${workflowID}/jobs?circle-token=${
process.env.CIRCLE_CI_API_TOKEN
}`
);
const {items: jobs} = await jobsResponse.json();
for (let j = 0; j < jobs.length; j++) {
const job = jobs[j];
if (job.name === 'process_artifacts') {
// Found it!
const baseCIBuildId = job.job_number;
const baseArtifactsInfoResponse = await fetch(
`https://circleci.com/api/v1.1/project/github/facebook/react/${baseCIBuildId}/artifacts?circle-token=${
process.env.CIRCLE_CI_API_TOKEN
}`
);
baseArtifactsInfo = await baseArtifactsInfoResponse.json();
break findArtifactsInfo;
}
}
}
}

if (baseCIBuildId === null) {
if (baseArtifactsInfo === null) {
warn(`Could not find build artifacts for base commit: ${baseCommit}`);
return;
}

const baseArtifactsInfoResponse = await fetch(
`https://circleci.com/api/v1.1/project/github/facebook/react/${baseCIBuildId}/artifacts`
);
const baseArtifactsInfo = await baseArtifactsInfoResponse.json();

for (let i = 0; i < baseArtifactsInfo.length; i++) {
const info = baseArtifactsInfo[i];
if (info.path === 'home/circleci/project/build/bundle-sizes.json') {
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/ReactFeatureFlags.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const disableInputAttributeSyncing = false;

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

export const warnAboutShorthandPropertyCollision = false;

Expand Down
1 change: 1 addition & 0 deletions scripts/flow/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

declare var __PROFILE__: boolean;
declare var __UMD__: boolean;
declare var __EXPERIMENTAL__: boolean;

declare var __REACT_DEVTOOLS_GLOBAL_HOOK__: any; /*?{
inject: ?((stuff: Object) => void)
Expand Down
1 change: 1 addition & 0 deletions scripts/jest/setupEnvironment.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ if (NODE_ENV !== 'development' && NODE_ENV !== 'production') {
global.__DEV__ = NODE_ENV === 'development';
global.__PROFILE__ = NODE_ENV === 'development';
global.__UMD__ = false;
global.__EXPERIMENTAL__ = process.env.RELEASE_CHANNEL === 'experimental';

if (typeof window !== 'undefined') {
global.requestIdleCallback = function(callback) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const run = async () => {
entry =>
entry.branch === 'master' &&
entry.status === 'success' &&
entry.workflows.workflow_name === 'build_stable' &&
entry.workflows.job_name === 'process_artifacts'
).build_num;

Expand Down
10 changes: 8 additions & 2 deletions scripts/release/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,16 @@ const getArtifactsList = async buildID => {
const getBuildInfo = async () => {
const cwd = join(__dirname, '..', '..');

const isExperimental = process.env.RELEASE_CHANNEL === 'experimental';

const branch = await execRead('git branch | grep \\* | cut -d " " -f2', {
cwd,
});
const commit = await execRead('git show -s --format=%h', {cwd});
const checksum = await getChecksumForCurrentRevision(cwd);
const version = `0.0.0-${commit}`;
const version = isExperimental
? `0.0.0-experimental-${commit}`
: `0.0.0-${commit}`;

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

return {branch, buildNumber, checksum, commit, reactVersion, version};
};
Expand Down
9 changes: 7 additions & 2 deletions scripts/rollup/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,8 @@ function getPlugins(
bundleType,
globalName,
moduleType,
pureExternalModules
pureExternalModules,
isExperimentalBuild
) {
const findAndRecordErrorCodes = extractErrorCodes(errorCodeOpts);
const forks = Modules.getForks(bundleType, entry, moduleType);
Expand Down Expand Up @@ -362,6 +363,7 @@ function getPlugins(
__PROFILE__: isProfiling || !isProduction ? 'true' : 'false',
__UMD__: isUMDBundle ? 'true' : 'false',
'process.env.NODE_ENV': isProduction ? "'production'" : "'development'",
__EXPERIMENTAL__: isExperimentalBuild,
}),
// We still need CommonJS for external deps like object-assign.
commonjs(),
Expand Down Expand Up @@ -485,6 +487,8 @@ async function createBundle(bundle, bundleType) {
module => !importSideEffects[module]
);

const isExperimentalBuild = process.env.RELEASE_CHANNEL === 'experimental';

const rollupConfig = {
input: resolvedEntry,
treeshake: {
Expand All @@ -508,7 +512,8 @@ async function createBundle(bundle, bundleType) {
bundleType,
bundle.global,
bundle.moduleType,
pureExternalModules
pureExternalModules,
isExperimentalBuild
),
// We can't use getters in www.
legacy:
Expand Down

0 comments on commit 8c4854d

Please sign in to comment.