Skip to content

Commit 33b31b1

Browse files
authored
build: snapshot builds incorrectly modify semver versions (#20053)
An interesting case that came up in v10.0.0 with the docs-content. The snapshot release package and docs-content output had various `@breaking-change` notes in the source code referring to `v10.0.0` as certain changes are planned to be made at that point. The snapshot deploy scripts pick up the version from the package.json file and replace it in the output packages with a more concrete version that includes the SHA. This meant that we accidentally also overwrote versions as in the `@breaking-change` notes (ultimately making it difficult for us to use the latest docs-content for the v10 release as it was incorrect). Example snapshot commit: angular/material2-docs-content@e624c71 We fix this by not including the SHA as part of the deployment, but rather including the SHA when building the NPM packages. At that point, we can safely just replace instances of the `0.0.0-PLACEHOLDER` without having to worry about accidental version overriding. To achieve this, we update our release stamping script to have two modes. i.e. snapshot build mode and release mode. Framework does this by checking the Git tag history but that seems less ideal as it makes the release output building reliant on external factors while our stamping is self-contained within the checked out code revision.
1 parent 092b151 commit 33b31b1

File tree

10 files changed

+88
-66
lines changed

10 files changed

+88
-66
lines changed

.bazelrc

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,20 @@ query --output=label_kind
3737
# By default, failing tests don't print any output, it goes to the log file
3838
test --test_output=errors
3939

40-
#################################
41-
# Release configuration. #
42-
# Run with "--config=release" #
43-
#################################
40+
####################################
41+
# Stamping configurations. #
42+
# Run with "--config=release" or #
43+
# "--config=snapshot-build". #
44+
####################################
4445

4546
# Configures script to do version stamping.
4647
# See https://docs.bazel.build/versions/master/user-manual.html#flag--workspace_status_command
4748
build:release --workspace_status_command="node ./tools/bazel-stamp-vars.js"
4849
build:release --stamp
4950

51+
build:snapshot-build --workspace_status_command="node ./tools/bazel-stamp-vars.js --snapshot"
52+
build:snapshot-build --stamp
53+
5054
################################
5155
# View Engine / Ivy toggle #
5256
################################

.circleci/config.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,12 @@ jobs:
359359
- *setup_bazel_binary
360360

361361
- run: yarn build
362-
- run: yarn check-release-output
362+
- run:
363+
name: Checking release output
364+
command: |
365+
pkg_json_version=$(node -pe "require('./package.json').version")
366+
expected_version="${pkg_json_version}-sha-$(git rev-parse --short HEAD)"
367+
yarn check-release-output ${expected_version}
363368
364369
# TODO(devversion): replace this with bazel tests that run Madge. This is
365370
# cumbersome and doesn't guarantee no circular deps for other entry-points.
@@ -435,7 +440,7 @@ jobs:
435440
# The components examples package is not a release package, but we publish it
436441
# as part of this job to the docs-content repository. It's not contained in the
437442
# attached release output, so we need to build it here.
438-
- run: bazel build src/components-examples:npm_package --config=release
443+
- run: bazel build src/components-examples:npm_package --config=snapshot-build
439444

440445
# Ensures that we do not push the snapshot artifacts upstream until all previous
441446
# snapshot build jobs have completed. This helps avoiding conflicts when multiple

scripts/build-packages-dist.js

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,28 +27,39 @@ const queryPackagesCmd =
2727
`${bazelCmd} query --output=label "attr('tags', '\\[.*${releaseTargetTag}.*\\]', //src/...) ` +
2828
`intersect kind('.*_package', //src/...)"`;
2929

30+
/** Path for the default distribution output directory. */
31+
const defaultDistPath = join(projectDir, 'dist/releases');
32+
3033
// Export the methods for building the release packages. These
3134
// can be consumed by the release tool.
32-
exports.buildReleasePackages = buildReleasePackages;
33-
exports.defaultBuildReleasePackages = defaultBuildReleasePackages;
35+
exports.performNpmReleaseBuild = performNpmReleaseBuild;
36+
exports.performDefaultSnapshotBuild = performDefaultSnapshotBuild;
3437

3538
if (module === require.main) {
36-
defaultBuildReleasePackages();
39+
// We always build as a snapshot bu8ild, unless the script is invoked directly by the
40+
// release publish script. The snapshot release configuration ensures that the current
41+
// Git `HEAD` sha is included for the version placeholders.
42+
performDefaultSnapshotBuild();
43+
}
44+
45+
/** Builds the release packages for NPM. */
46+
function performNpmReleaseBuild() {
47+
buildReleasePackages(false, defaultDistPath, /* isSnapshotBuild */ false);
3748
}
3849

3950
/**
40-
* Builds the release packages with the default compile mode and
41-
* output directory.
51+
* Builds the release packages as snapshot build. This means that the current
52+
* Git HEAD SHA is included in the version (for easier debugging and back tracing).
4253
*/
43-
function defaultBuildReleasePackages() {
44-
buildReleasePackages(false, join(projectDir, 'dist/releases'));
54+
function performDefaultSnapshotBuild() {
55+
buildReleasePackages(false, defaultDistPath, /* isSnapshotBuild */ true);
4556
}
4657

4758
/**
4859
* Builds the release packages with the given compile mode and copies
4960
* the package output into the given directory.
5061
*/
51-
function buildReleasePackages(useIvy, distPath) {
62+
function buildReleasePackages(useIvy, distPath, isSnapshotBuild) {
5263
console.log('######################################');
5364
console.log(' Building release packages...');
5465
console.log(` Compiling with Ivy: ${useIvy}`);
@@ -60,6 +71,12 @@ function buildReleasePackages(useIvy, distPath) {
6071
const bazelBinPath = exec(`${bazelCmd} info bazel-bin`, true);
6172
const getOutputPath = pkgName => join(bazelBinPath, 'src', pkgName, 'npm_package');
6273

74+
// Build with "--config=release" or `--config=snapshot-build` so that Bazel
75+
// runs the workspace stamping script. The stamping script ensures that the
76+
// version placeholder is populated in the release output.
77+
const stampConfigArg = `--config=${isSnapshotBuild ? 'snapshot-build' : 'release'}`;
78+
const ivySwitchConfigArg = `--config=${useIvy ? 'ivy' : 'view-engine'}`;
79+
6380
// Walk through each release package and clear previous "npm_package" outputs. This is
6481
// a workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1219. We need to
6582
// do this to ensure that the version placeholders are properly populated.
@@ -71,9 +88,7 @@ function buildReleasePackages(useIvy, distPath) {
7188
}
7289
});
7390

74-
// Build with "--config=release" so that Bazel runs the workspace stamping script. The
75-
// stamping script ensures that the version placeholder is populated in the release output.
76-
exec(`${bazelCmd} build --config=release --config=${useIvy ? 'ivy' : 'view-engine'} ${targets.join(' ')}`);
91+
exec(`${bazelCmd} build ${stampConfigArg} ${ivySwitchConfigArg} ${targets.join(' ')}`);
7792

7893
// Delete the distribution directory so that the output is guaranteed to be clean. Re-create
7994
// the empty directory so that we can copy the release packages into it later.

scripts/deploy/publish-build-artifacts.sh

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,6 @@ publishPackage() {
4545
commitAuthorEmail=$(git --no-pager show -s --format='%ae' HEAD)
4646
commitMessage=$(git log --oneline -n 1)
4747

48-
# Note that we cannot store the commit SHA in its own version segment
49-
# as it will not comply with the semver specification. For example:
50-
# 1.0.0-00abcdef will break since the SHA starts with zeros. To fix this,
51-
# we create a new version segment with the following format: "1.0.0-sha-00abcdef".
52-
# See issue: https://jubianchi.github.io/semver-check/#/^8.0.0/8.2.2-0462599
5348
buildVersionName="${buildVersion}-sha-${commitSha}"
5449
buildTagName="${branchName}-${commitSha}"
5550
buildCommitMessage="${branchName} - ${commitMessage}"
@@ -99,12 +94,6 @@ publishPackage() {
9994
exit 0
10095
fi
10196

102-
# Replace the version in every file recursively with a more specific version that also includes
103-
# the SHA of the current build job. Normally this "sed" call would just replace the version
104-
# placeholder, but the version placeholders have been replaced by "npm_package" already.
105-
escapedVersion=$(echo ${buildVersion} | sed 's/[.[\*^$]/\\&/g')
106-
sed -i "s/${escapedVersion}/${buildVersionName}/g" $(find . -type f -not -path '*\/.*')
107-
10897
echo "Updated the build version in every file to include the SHA of the latest commit."
10998

11099
# Prepare Git for pushing the artifacts to the repository.

scripts/deploy/publish-docs-content.sh

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,6 @@ commitAuthorName=$(git --no-pager show -s --format='%an' HEAD)
4141
commitAuthorEmail=$(git --no-pager show -s --format='%ae' HEAD)
4242
commitMessage=$(git log --oneline -n 1)
4343

44-
# Note that we cannot store the commit SHA in its own version segment
45-
# as it will not comply with the semver specification. For example:
46-
# 1.0.0-00abcdef will break since the SHA starts with zeros. To fix this,
47-
# we create a new version segment with the following format: "1.0.0-sha-00abcdef".
48-
# See issue: https://jubianchi.github.io/semver-check/#/^8.0.0/8.2.2-0462599
4944
buildVersionName="${buildVersion}-sha-${commitSha}"
5045
buildTagName="${branchName}-${commitSha}"
5146
buildCommitMessage="${branchName} - ${commitMessage}"
@@ -95,12 +90,6 @@ if [[ $(git ls-remote origin "refs/tags/${buildTagName}") ]]; then
9590
exit 0
9691
fi
9792

98-
# Replace the version in every file recursively with a more specific version that also includes
99-
# the SHA of the current build job. Normally this "sed" call would just replace the version
100-
# placeholder, but the version placeholders have been replaced by "npm_package" already.
101-
escapedVersion=$(echo ${buildVersion} | sed 's/[.[\*^$]/\\&/g')
102-
sed -i "s/${escapedVersion}/${buildVersionName}/g" $(find . -type f -not -path '*\/.*')
103-
10493
# Setup the Git configuration
10594
git config user.name "$commitAuthorName"
10695
git config user.email "$commitAuthorEmail"

tools/bazel-stamp-vars.js

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,13 @@
99

1010
const spawnSync = require('child_process').spawnSync;
1111
const packageJson = require('../package');
12-
13-
const currentCommitSha = getCurrentCommitSha();
12+
const isSnapshotStamp = process.argv.slice(2).includes('--snapshot');
1413

1514
// The "BUILD_SCM_VERSION" will be picked up by the "npm_package" and "ng_package"
1615
// rule in order to populate the "0.0.0-PLACEHOLDER". Note that the SHA will be only
17-
// appended for snapshots builds from within the "publish-build-artifacts.sh" script.
18-
console.log(`BUILD_SCM_VERSION ${packageJson.version}`);
19-
console.log(`BUILD_SCM_COMMIT_SHA ${currentCommitSha}`);
16+
// appended for snapshots builds (if the `--snapshot` flag has been passed to this script).
17+
console.log(`BUILD_SCM_VERSION ${getBuildVersion()}`);
18+
console.log(`BUILD_SCM_COMMIT_SHA ${getCurrentCommitSha()}`);
2019
console.log(`BUILD_SCM_BRANCH ${getCurrentBranchName()}`);
2120
console.log(`BUILD_SCM_USER ${getCurrentGitUser()}`);
2221

@@ -25,6 +24,11 @@ function getCurrentCommitSha() {
2524
return spawnSync('git', ['rev-parse', 'HEAD']).stdout.toString().trim();
2625
}
2726

27+
/** Returns the abbreviated SHA for the current git HEAD of the project. */
28+
function getAbbreviatedCommitSha() {
29+
return spawnSync('git', ['rev-parse', '--short', 'HEAD']).stdout.toString().trim();
30+
}
31+
2832
/** Returns the name of the currently checked out branch of the project. */
2933
function getCurrentBranchName() {
3034
return spawnSync('git', ['symbolic-ref', '--short', 'HEAD']).stdout.toString().trim();
@@ -37,3 +41,17 @@ function getCurrentGitUser() {
3741

3842
return `${userName} <${userEmail}>`;
3943
}
44+
45+
/** Gets the version for the current build. */
46+
function getBuildVersion() {
47+
if (isSnapshotStamp) {
48+
// Note that we cannot store the commit SHA as prerelease segment as it will not comply
49+
// with the semver specification in some situations. For example: `1.0.0-00abcdef` will
50+
// break since the SHA starts with zeros. To fix this, we create a prerelease segment with
51+
// label where the SHA is considered part of the label and not the prerelease number.
52+
// Here is an example of the valid format: "1.0.0-sha-00abcdef".
53+
// See issue: https://jubianchi.github.io/semver-check/#/^8.0.0/8.2.2-0462599
54+
return `${packageJson.version}-sha-${getAbbreviatedCommitSha()}`;
55+
}
56+
return packageJson.version;
57+
}

tools/release/check-release-output.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,16 @@ import chalk from 'chalk';
22
import {join} from 'path';
33
import {checkReleasePackage} from './release-output/check-package';
44
import {releasePackages} from './release-output/release-packages';
5-
import {parseVersionName, Version} from './version-name/parse-version';
65

76
/**
87
* Checks the release output by running the release-output validations for each
98
* release package.
109
*/
11-
export function checkReleaseOutput(releaseOutputDir: string, currentVersion: Version) {
10+
export function checkReleaseOutput(releaseOutputDir: string, expectedVersion: string) {
1211
let hasFailed = false;
1312

1413
releasePackages.forEach(packageName => {
15-
if (!checkReleasePackage(releaseOutputDir, packageName, currentVersion)) {
14+
if (!checkReleasePackage(releaseOutputDir, packageName, expectedVersion)) {
1615
hasFailed = true;
1716
}
1817
});
@@ -30,9 +29,10 @@ export function checkReleaseOutput(releaseOutputDir: string, currentVersion: Ver
3029

3130

3231
if (require.main === module) {
33-
const currentVersion = parseVersionName(require('../../package.json').version);
34-
if (currentVersion === null) {
35-
throw Error('Version in project "package.json" is invalid.');
32+
const [expectedVersion] = process.argv.slice(2);
33+
if (expectedVersion === undefined) {
34+
console.error('No expected version specified. Please pass one as argument.');
35+
process.exit(1);
3636
}
37-
checkReleaseOutput(join(__dirname, '../../dist/releases'), currentVersion);
37+
checkReleaseOutput(join(__dirname, '../../dist/releases'), expectedVersion);
3838
}

tools/release/publish-release.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {parseVersionName, Version} from './version-name/parse-version';
1616

1717
// The package builder script is not written in TypeScript and needs to
1818
// be imported through a CommonJS import.
19-
const {defaultBuildReleasePackages} = require('../../scripts/build-packages-dist');
19+
const {performNpmReleaseBuild} = require('../../scripts/build-packages-dist');
2020

2121
/**
2222
* Class that can be instantiated in order to create a new release. The tasks requires user
@@ -90,11 +90,11 @@ class PublishReleaseTask extends BaseReleaseTask {
9090
await this._promptStableVersionForNextTag();
9191
}
9292

93-
defaultBuildReleasePackages();
93+
performNpmReleaseBuild();
9494
console.info(chalk.green(` ✓ Built the release output.`));
9595

9696
// Checks all release packages against release output validations before releasing.
97-
checkReleaseOutput(this.releaseOutputPath, this.currentVersion);
97+
checkReleaseOutput(this.releaseOutputPath, this.currentVersion.format());
9898

9999
// Extract the release notes for the new version from the changelog file.
100100
const extractedReleaseNotes = extractReleaseNotes(

tools/release/release-output/check-package.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import chalk from 'chalk';
22
import {existsSync} from 'fs';
33
import {sync as glob} from 'glob';
44
import {join} from 'path';
5-
import {Version} from '../version-name/parse-version';
65

76
import {
87
checkCdkPackage,
@@ -35,7 +34,7 @@ type PackageFailures = Map<string, string[]>;
3534
* @returns Whether the package passed all checks or not.
3635
*/
3736
export function checkReleasePackage(
38-
releasesPath: string, packageName: string, currentVersion: Version): boolean {
37+
releasesPath: string, packageName: string, expectedVersion: string): boolean {
3938
const packagePath = join(releasesPath, packageName);
4039
const failures = new Map() as PackageFailures;
4140
const addFailure = (message, filePath?) => {
@@ -82,7 +81,7 @@ export function checkReleasePackage(
8281
addFailure('No "README.md" file found in package output.');
8382
}
8483

85-
checkPrimaryPackageJson(join(packagePath, 'package.json'), currentVersion)
84+
checkPrimaryPackageJson(join(packagePath, 'package.json'), expectedVersion)
8685
.forEach(f => addFailure(f));
8786

8887
// In case there are failures for this package, we want to print those

tools/release/release-output/output-validations.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,7 @@ export function checkTypeDefinitionFile(filePath: string): string[] {
115115
* that the version and migrations are set up correctly.
116116
*/
117117
export function checkPrimaryPackageJson(
118-
packageJsonPath: string, currentVersion: Version): string[] {
119-
const expectedVersion = currentVersion.format();
118+
packageJsonPath: string, expectedVersion: string): string[] {
120119
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
121120
const failures: string[] = [];
122121

@@ -125,11 +124,12 @@ export function checkPrimaryPackageJson(
125124
} else if (packageJson.version !== expectedVersion) {
126125
failures.push(
127126
`Unexpected package version. Expected: ${expectedVersion} but got: ${packageJson.version}`);
128-
}
129-
130-
if (packageJson['ng-update'] && packageJson['ng-update'].migrations) {
127+
} else if (semver.valid(expectedVersion) === null) {
128+
failures.push(`Version does not satisfy SemVer specification: ${packageJson.version}`);
129+
} else if (packageJson['ng-update'] && packageJson['ng-update'].migrations) {
131130
failures.push(...checkMigrationCollection(
132-
packageJson['ng-update'].migrations, dirname(packageJsonPath), currentVersion));
131+
packageJson['ng-update'].migrations, dirname(packageJsonPath),
132+
semver.parse(expectedVersion)!));
133133
}
134134

135135
return failures;
@@ -144,8 +144,11 @@ export function checkPackageJsonMigrations(
144144
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
145145

146146
if (packageJson['ng-update'] && packageJson['ng-update'].migrations) {
147+
// TODO(devversion): switch release publish tooling to use `SemVer` instead
148+
// of custom version parsing/serializing.
147149
return checkMigrationCollection(
148-
packageJson['ng-update'].migrations, dirname(packageJsonPath), currentVersion);
150+
packageJson['ng-update'].migrations, dirname(packageJsonPath),
151+
semver.parse(currentVersion.format())!);
149152
}
150153
return [];
151154
}
@@ -185,7 +188,7 @@ export function checkCdkPackage(packagePath: string): string[] {
185188
* has a migration set up for the given target version.
186189
*/
187190
function checkMigrationCollection(
188-
collectionPath: string, packagePath: string, targetVersion: Version): string[] {
191+
collectionPath: string, packagePath: string, targetVersion: semver.SemVer): string[] {
189192
const collection = JSON.parse(readFileSync(join(packagePath, collectionPath), 'utf8'));
190193
if (!collection.schematics) {
191194
return ['No schematics found in migration collection.'];
@@ -198,7 +201,7 @@ function checkMigrationCollection(
198201
const schematicVersion = schematics[name].version;
199202
try {
200203
return schematicVersion && semver.gte(schematicVersion, lowerBoundaryVersion) &&
201-
semver.lte(schematicVersion, targetVersion.format());
204+
semver.lte(schematicVersion, targetVersion);
202205
} catch {
203206
failures.push(`Could not parse version for migration: ${name}`);
204207
}

0 commit comments

Comments
 (0)