From 8c4854d8cac27c5d4ed1facd17faa144c98bda11 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 11 Oct 2019 15:25:13 -0700 Subject: [PATCH] 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. --- .circleci/config.yml | 55 ++++++++++++++++- .eslintrc.js | 1 + dangerfile.js | 59 +++++++++++++------ packages/shared/ReactFeatureFlags.js | 2 +- scripts/flow/environment.js | 1 + scripts/jest/setupEnvironment.js | 1 + .../get-latest-master-build-number.js | 1 + scripts/release/utils.js | 10 +++- scripts/rollup/build.js | 9 ++- 9 files changed, 113 insertions(+), 26 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 535563556ee95..ad8a52d1b5125 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 @@ -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 @@ -208,7 +244,7 @@ jobs: workflows: version: 2 - commit: + stable: jobs: - setup - lint: @@ -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: diff --git a/.eslintrc.js b/.eslintrc.js index 603f20a3cacba..df147c90bd3e6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -149,6 +149,7 @@ module.exports = { spyOnProd: true, __PROFILE__: true, __UMD__: true, + __EXPERIMENTAL__: true, trustedTypes: true, }, }; diff --git a/dangerfile.js b/dangerfile.js index 985c6b675626d..3eee061d5cf34 100644 --- a/dangerfile.js +++ b/dangerfile.js @@ -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` ); @@ -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') { diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index c1645f0497674..ddb12f71b7993 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -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; diff --git a/scripts/flow/environment.js b/scripts/flow/environment.js index 8eeda9784000d..0a742aad4022f 100644 --- a/scripts/flow/environment.js +++ b/scripts/flow/environment.js @@ -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) diff --git a/scripts/jest/setupEnvironment.js b/scripts/jest/setupEnvironment.js index 10ff1234b4c72..1e694b2d31575 100644 --- a/scripts/jest/setupEnvironment.js +++ b/scripts/jest/setupEnvironment.js @@ -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) { diff --git a/scripts/release/prepare-canary-commands/get-latest-master-build-number.js b/scripts/release/prepare-canary-commands/get-latest-master-build-number.js index a87361ecbc74a..574ec98184c74 100644 --- a/scripts/release/prepare-canary-commands/get-latest-master-build-number.js +++ b/scripts/release/prepare-canary-commands/get-latest-master-build-number.js @@ -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; diff --git a/scripts/release/utils.js b/scripts/release/utils.js index 861b83ad34d6c..816f0fdfc2853 100644 --- a/scripts/release/utils.js +++ b/scripts/release/utils.js @@ -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/ @@ -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}; }; diff --git a/scripts/rollup/build.js b/scripts/rollup/build.js index 92f1f3a754648..34184efab63e1 100644 --- a/scripts/rollup/build.js +++ b/scripts/rollup/build.js @@ -304,7 +304,8 @@ function getPlugins( bundleType, globalName, moduleType, - pureExternalModules + pureExternalModules, + isExperimentalBuild ) { const findAndRecordErrorCodes = extractErrorCodes(errorCodeOpts); const forks = Modules.getForks(bundleType, entry, moduleType); @@ -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(), @@ -485,6 +487,8 @@ async function createBundle(bundle, bundleType) { module => !importSideEffects[module] ); + const isExperimentalBuild = process.env.RELEASE_CHANNEL === 'experimental'; + const rollupConfig = { input: resolvedEntry, treeshake: { @@ -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: