diff --git a/.bazelrc b/.bazelrc index 9395ec6d0..29329576c 100644 --- a/.bazelrc +++ b/.bazelrc @@ -63,6 +63,14 @@ build:remote --platforms=//bazel/remote-execution:platform_with_network # Set remote caching settings build:remote --remote_accept_cached=true +################################ +# Release setup # +################################ + +# Releases should always be stamped with version control info +build:release --workspace_status_command="yarn -s ng-dev release build-env-stamp --mode=release" +build:release --stamp + #################################################### # User bazel configuration # NOTE: This needs to be the *last* entry in the config to allow for it to override other diff --git a/.circleci/config.yml b/.circleci/config.yml index 5c6a523a0..dae356913 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -134,7 +134,7 @@ jobs: - yarn_install - run: name: Build npm package - command: yarn bazel build //:npm_package + command: yarn bazel build //:npm_package --config=release - run: name: Publish snapshot build to github command: ./.circleci/publish_to_github.sh @@ -150,3 +150,6 @@ workflows: branches: only: - main + # Additional branch that can be used to test the snapshot build output. + # Developers can just push to that branch to test the built artifact. + - snapshot-test diff --git a/BUILD.bazel b/BUILD.bazel index fad73dd0b..d2ca07324 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -1,4 +1,5 @@ # BEGIN-INTERNAL +load("//:package.bzl", "NPM_PACKAGE_SUBSTITUTIONS") load("//tools:defaults.bzl", "pkg_npm") load("@npm//@bazel/typescript:index.bzl", "ts_config") @@ -25,17 +26,7 @@ pkg_npm( ":index.bzl", "//bazel:static_files", ], - substitutions = { - " \"prepare\": \"husky install\",\n": "", - "@dev-infra//bazel/": "@npm//@angular/dev-infra-private/bazel/", - "//bazel/": "@npm//@angular/dev-infra-private/bazel/", - "//bazel:": "@npm//@angular/dev-infra-private/bazel:", - "//ng-dev/": "@npm//@angular/dev-infra-private/ng-dev/", - "//ng-dev:": "@npm//@angular/dev-infra-private/ng-dev:", - "//tslint-rules/": "@npm//@angular/dev-infra-private/tslint-rules/", - "//tslint-rules:": "@npm//@angular/dev-infra-private/tslint-rules:", - "//:tsconfig.json": "@npm//@angular/dev-infra-private:tsconfig.json", - }, + substitutions = NPM_PACKAGE_SUBSTITUTIONS, deps = [ "//ng-dev", "//ng-dev:lib", diff --git a/ng-dev/release/publish/BUILD.bazel b/ng-dev/release/publish/BUILD.bazel index 7970bf90c..4d9da4563 100644 --- a/ng-dev/release/publish/BUILD.bazel +++ b/ng-dev/release/publish/BUILD.bazel @@ -19,6 +19,8 @@ ts_library( "@npm//@types/node", "@npm//@types/semver", "@npm//@types/yargs", + "@npm//@types/yarnpkg__lockfile", + "@npm//@yarnpkg/lockfile", "@npm//ejs", "@npm//inquirer", "@npm//semver", diff --git a/ng-dev/release/publish/constants.ts b/ng-dev/release/publish/constants.ts index 9791cb065..098d3ecca 100644 --- a/ng-dev/release/publish/constants.ts +++ b/ng-dev/release/publish/constants.ts @@ -9,6 +9,9 @@ /** Project-relative path for the "package.json" file. */ export const packageJsonPath = 'package.json'; +/** Project-relative path for the "yarn.lock" file. */ +export const yarnLockFilePath = 'yarn.lock'; + /** Default interval in milliseconds to check whether a pull request has been merged. */ export const waitForPullRequestInterval = 10000; diff --git a/ng-dev/release/publish/external-commands.ts b/ng-dev/release/publish/external-commands.ts index 132693880..402f9df3e 100644 --- a/ng-dev/release/publish/external-commands.ts +++ b/ng-dev/release/publish/external-commands.ts @@ -97,31 +97,3 @@ export async function invokeYarnInstallCommand(projectDir: string): Promise { - try { - await spawn('yarn', ['check', '--integrity'], {cwd: projectDir, mode: 'silent'}); - info(green(' ✓ Confirmed dependencies from package.json match those in yarn.lock.')); - } catch (e) { - error(red(' ✘ Failed yarn integrity check, your installed dependencies are likely out of')); - error(red(' date. Please run `yarn install` to update your installed dependencies.')); - throw new FatalReleaseActionError(); - } -} - -/** - * Invokes the `yarn check --verify-tree` command in order to verify up to date dependencies. - */ -export async function invokeYarnVerifyTreeCheck(projectDir: string): Promise { - try { - await spawn('yarn', ['check', '--verify-tree'], {cwd: projectDir, mode: 'silent'}); - info(green(' ✓ Confirmed installed dependencies match those defined in package.json.')); - } catch (e) { - error(red(' ✘ Failed yarn verify tree check, your installed dependencies are likely out')); - error(red(' of date. Please run `yarn install` to update your installed dependencies.')); - throw new FatalReleaseActionError(); - } -} diff --git a/ng-dev/release/publish/index.ts b/ng-dev/release/publish/index.ts index 2008bf01c..6e20d1c5c 100644 --- a/ng-dev/release/publish/index.ts +++ b/ng-dev/release/publish/index.ts @@ -6,7 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ +import * as path from 'path'; +import * as fs from 'fs'; import {ListChoiceOptions, prompt} from 'inquirer'; +import {parse as parseYarnLockfile, LockFileObject} from '@yarnpkg/lockfile'; + import {GithubConfig} from '../../utils/config'; import {debug, error, info, log, promptConfirm, red, yellow} from '../../utils/console'; import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client'; @@ -15,11 +19,12 @@ import {ActiveReleaseTrains, fetchActiveReleaseTrains} from '../versioning/activ import {npmIsLoggedIn, npmLogin, npmLogout} from '../versioning/npm-publish'; import {printActiveReleaseTrains} from '../versioning/print-active-trains'; import {getNextBranchName, ReleaseRepoWithApi} from '../versioning/version-branches'; +import {ngDevNpmPackageName} from '../../utils/constants'; import {ReleaseAction} from './actions'; import {FatalReleaseActionError, UserAbortedReleaseActionError} from './actions-error'; import {actions} from './actions/index'; -import {invokeYarnIntegrityCheck, invokeYarnVerifyTreeCheck} from './external-commands'; +import {packageJsonPath, yarnLockFilePath} from './constants'; export enum CompletionState { SUCCESS, @@ -152,11 +157,44 @@ export class ReleaseTool { * @returns a boolean indicating success or failure. */ private async _verifyInstalledDependenciesAreUpToDate(): Promise { + // The placeholder will be replaced by the `pkg_npm` substitutions. + const localVersion = `0.0.0-{SCM_HEAD_SHA}`; + const projectPackageJsonFile = path.join(this._projectRoot, packageJsonPath); + const projectDirLockFile = path.join(this._projectRoot, yarnLockFilePath); + try { - await invokeYarnVerifyTreeCheck(this._projectRoot); - await invokeYarnIntegrityCheck(this._projectRoot); + const lockFileContent = fs.readFileSync(projectDirLockFile, 'utf8'); + const packageJson = JSON.parse(fs.readFileSync(projectPackageJsonFile, 'utf8')) as any; + const lockFile = parseYarnLockfile(lockFileContent); + + if (lockFile.type !== 'success') { + throw Error('Unable to parse project lock file. Please ensure the file is valid.'); + } + + // If we are operating in the actual dev-infra repo, always return `true`. + if (packageJson.name === ngDevNpmPackageName) { + return true; + } + + const lockFileObject = lockFile.object as LockFileObject; + const devInfraPkgVersion = + packageJson?.dependencies?.[ngDevNpmPackageName] ?? + packageJson?.devDependencies?.[ngDevNpmPackageName] ?? + packageJson?.optionalDependencies?.[ngDevNpmPackageName]; + const expectedVersion = + lockFileObject[`${ngDevNpmPackageName}@${devInfraPkgVersion}`].version; + + if (localVersion !== expectedVersion) { + error(red(' ✘ Your locally installed version of the `ng-dev` tool is outdated and not')); + error(red(' matching with the version in the `package.json` file.')); + error( + red(' Re-install the dependencies to ensure you are using the correct version.'), + ); + return false; + } return true; - } catch { + } catch (e) { + error(e); return false; } } diff --git a/ng-dev/release/publish/yarn-lock-file.d.ts b/ng-dev/release/publish/yarn-lock-file.d.ts new file mode 100644 index 000000000..4d41bbe2a --- /dev/null +++ b/ng-dev/release/publish/yarn-lock-file.d.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1033 +// TODO: Remove when the type resolution is fixed. +declare module '@yarnpkg/lockfile' { + export * from 'yarnpkg__lockfile'; +} diff --git a/ng-dev/release/stamping/env-stamp.ts b/ng-dev/release/stamping/env-stamp.ts index 43664a7d9..45ecf4330 100644 --- a/ng-dev/release/stamping/env-stamp.ts +++ b/ng-dev/release/stamping/env-stamp.ts @@ -26,8 +26,9 @@ export type EnvStampMode = 'snapshot' | 'release'; export function buildEnvStamp(mode: EnvStampMode) { const git = GitClient.get(); console.info(`BUILD_SCM_BRANCH ${getCurrentBranch(git)}`); - console.info(`BUILD_SCM_COMMIT_SHA ${getCurrentBranchOrRevision(git)}`); - console.info(`BUILD_SCM_HASH ${getCurrentBranchOrRevision(git)}`); + console.info(`BUILD_SCM_COMMIT_SHA ${getCurrentSha(git)}`); + console.info(`BUILD_SCM_HASH ${getCurrentSha(git)}`); + console.info(`BUILD_SCM_BRANCH ${getCurrentBranchOrRevision(git)}`); console.info(`BUILD_SCM_LOCAL_CHANGES ${hasLocalChanges(git)}`); console.info(`BUILD_SCM_USER ${getCurrentGitUser(git)}`); const {version, experimentalVersion} = getSCMVersions(git, mode); @@ -89,6 +90,15 @@ function getSCMVersions( } } +/** Get the current SHA of HEAD. */ +function getCurrentSha(git: GitClient) { + try { + return git.run(['rev-parse', 'HEAD']).stdout.trim(); + } catch { + return ''; + } +} + /** Get the current branch or revision of HEAD. */ function getCurrentBranchOrRevision(git: GitClient) { try { diff --git a/ng-dev/utils/constants.ts b/ng-dev/utils/constants.ts new file mode 100644 index 000000000..7091df11e --- /dev/null +++ b/ng-dev/utils/constants.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** NPM package name that is used for the `ng-dev` tool. */ +export const ngDevNpmPackageName = '@angular/dev-infra-private'; diff --git a/package.bzl b/package.bzl new file mode 100644 index 000000000..07f56af14 --- /dev/null +++ b/package.bzl @@ -0,0 +1,24 @@ +stampSubstitutions = { + # The variables are special statuses generated within the Bazel workspace + # status command stamping script. + "{SCM_HEAD_SHA}": "{BUILD_SCM_COMMIT_SHA}", +} + +noStampSubstitutions = dict(stampSubstitutions, **{}) + +basePackageSubstitutions = { + " \"prepare\": \"husky install\",\n": "", + "@dev-infra//bazel/": "@npm//@angular/dev-infra-private/bazel/", + "//bazel/": "@npm//@angular/dev-infra-private/bazel/", + "//bazel:": "@npm//@angular/dev-infra-private/bazel:", + "//ng-dev/": "@npm//@angular/dev-infra-private/ng-dev/", + "//ng-dev:": "@npm//@angular/dev-infra-private/ng-dev:", + "//tslint-rules/": "@npm//@angular/dev-infra-private/tslint-rules/", + "//tslint-rules:": "@npm//@angular/dev-infra-private/tslint-rules:", + "//:tsconfig.json": "@npm//@angular/dev-infra-private:tsconfig.json", +} + +NPM_PACKAGE_SUBSTITUTIONS = select({ + "//tools:stamp": dict(basePackageSubstitutions, **stampSubstitutions), + "//conditions:default": dict(basePackageSubstitutions, **noStampSubstitutions), +}) diff --git a/package.json b/package.json index e5b267567..1e1a347ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/dev-infra-private", - "version": "0.0.0", + "version": "0.0.0-{SCM_HEAD_SHA}", "bin": { "ng-dev": "./ng-dev/cli-bundle.js" }, @@ -34,6 +34,7 @@ "@rollup/plugin-commonjs": "^21.0.0", "@rollup/plugin-node-resolve": "^13.0.4", "@types/tmp": "^0.2.1", + "@yarnpkg/lockfile": "^1.1.0", "chalk": "^4.1.0", "clang-format": "^1.4.0", "cli-progress": "^3.7.0", @@ -80,6 +81,7 @@ "@types/shelljs": "^0.8.8", "@types/uuid": "^8.3.1", "@types/yargs": "^17.0.0", + "@types/yarnpkg__lockfile": "^1.1.5", "jsdoc": "^3.6.7", "minimist": "^1.2.5", "protobufjs": "^6.11.2", diff --git a/tools/BUILD.bazel b/tools/BUILD.bazel index 812345d7a..d7f798c4b 100644 --- a/tools/BUILD.bazel +++ b/tools/BUILD.bazel @@ -1 +1,7 @@ -# This BUILD.bazel file needs to exist to allow for //tools to be considered a package. +package(default_visibility = ["//visibility:public"]) + +# Detect if the build is running with stamping enabled. +config_setting( + name = "stamp", + values = {"stamp": "true"}, +) diff --git a/yarn.lock b/yarn.lock index ed2bda6f6..bf49a11a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -708,6 +708,16 @@ dependencies: "@types/yargs-parser" "*" +"@types/yarnpkg__lockfile@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.5.tgz#9639020e1fb65120a2f4387db8f1e8b63efdf229" + integrity sha512-8NYnGOctzsI4W0ApsP/BIHD/LnxpJ6XaGf2AZmz4EyDYJMxtprN4279dLNI1CPZcwC9H18qYcaFv4bXi0wmokg== + +"@yarnpkg/lockfile@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" + integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== + JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"