diff --git a/README.md b/README.md index 685fe0d6d..090f8c2d3 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,7 @@ Release Please automates releases for the following flavors of repositories: | release type | description | |---------------------|---------------------------------------------------------| +| `bazel` | [A Bazel module, with a MODULE.bazel and a CHANGELOG.md](https://bazel.build/external/module) | | `dart` | A repository with a pubspec.yaml and a CHANGELOG.md | | `elixir` | A repository with a mix.exs and a CHANGELOG.md | | `go` | A repository with a CHANGELOG.md | diff --git a/__snapshots__/cli.js b/__snapshots__/cli.js index d30de7546..60389f1fa 100644 --- a/__snapshots__/cli.js +++ b/__snapshots__/cli.js @@ -40,8 +40,8 @@ Options: [string] --release-type what type of repo is a release being created for? - [choices: "dart", "dotnet-yoshi", "elixir", "expo", "go", "go-yoshi", "helm", - "java", "java-backport", "java-bom", "java-lts", "java-yoshi", + [choices: "bazel", "dart", "dotnet-yoshi", "elixir", "expo", "go", "go-yoshi", + "helm", "java", "java-backport", "java-bom", "java-lts", "java-yoshi", "java-yoshi-mono-repo", "krm-blueprint", "maven", "node", "ocaml", "php", "php-yoshi", "python", "ruby", "ruby-yoshi", "rust", "salesforce", "sfdx", "simple", "terraform-module"] @@ -241,8 +241,8 @@ Options: [string] --release-type what type of repo is a release being created for? - [choices: "dart", "dotnet-yoshi", "elixir", "expo", "go", "go-yoshi", "helm", - "java", "java-backport", "java-bom", "java-lts", "java-yoshi", + [choices: "bazel", "dart", "dotnet-yoshi", "elixir", "expo", "go", "go-yoshi", + "helm", "java", "java-backport", "java-bom", "java-lts", "java-yoshi", "java-yoshi-mono-repo", "krm-blueprint", "maven", "node", "ocaml", "php", "php-yoshi", "python", "ruby", "ruby-yoshi", "rust", "salesforce", "sfdx", "simple", "terraform-module"] diff --git a/__snapshots__/module-bazel.js b/__snapshots__/module-bazel.js new file mode 100644 index 000000000..329e470e2 --- /dev/null +++ b/__snapshots__/module-bazel.js @@ -0,0 +1,45 @@ +exports['ModuleBazel updateContent updates version in MODULE.bazel file 1'] = ` +module( + name = "rules_cc", + version = "0.0.5", + compatibility_level = 1, +) + +bazel_dep(name = "bazel_skylib", version = "1.3.0") +bazel_dep(name = "platforms", version = "0.0.7") + +cc_configure = use_extension("@bazel_tools//tools/cpp:cc_configure.bzl", "cc_configure_extension") +use_repo(cc_configure, "local_config_cc_toolchains") + +register_toolchains("@local_config_cc_toolchains//:all") + +bazel_dep(name = "rules_testing", version = "0.6.0", dev_dependency = True) +` + +exports['ModuleBazel updateContent updates version when inline 1'] = ` +module(name = "rules_cc", version = "0.0.5") + +bazel_dep(name = "bazel_skylib", version = "1.3.0") +bazel_dep(name = "platforms", version = "0.0.7") + +cc_configure = use_extension("@bazel_tools//tools/cpp:cc_configure.bzl", "cc_configure_extension") +use_repo(cc_configure, "local_config_cc_toolchains") + +register_toolchains("@local_config_cc_toolchains//:all") + +bazel_dep(name = "rules_testing", version = "0.6.0", dev_dependency = True) +` + +exports['ModuleBazel updateContent updates version when ordered improperly 1'] = ` +bazel_dep(name = "bazel_skylib", version = "1.3.0") +bazel_dep(name = "platforms", version = "0.0.7") + +cc_configure = use_extension("@bazel_tools//tools/cpp:cc_configure.bzl", "cc_configure_extension") +use_repo(cc_configure, "local_config_cc_toolchains") + +module(name = "rules_cc", version = "0.0.5") + +register_toolchains("@local_config_cc_toolchains//:all") + +bazel_dep(name = "rules_testing", version = "0.6.0", dev_dependency = True) +` diff --git a/src/factory.ts b/src/factory.ts index db5ae352f..effc07ca7 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -12,12 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {Strategy} from './strategy'; +import {ConfigurationError} from './errors'; +import {buildChangelogNotes} from './factories/changelog-notes-factory'; +import {buildVersioningStrategy} from './factories/versioning-strategy-factory'; +import {GitHub} from './github'; +import {ReleaserConfig} from './manifest'; +import {BaseStrategyOptions} from './strategies/base'; +import {Bazel} from './strategies/bazel'; +import {Dart} from './strategies/dart'; +import {DotnetYoshi} from './strategies/dotnet-yoshi'; +import {Elixir} from './strategies/elixir'; +import {Expo} from './strategies/expo'; import {Go} from './strategies/go'; import {GoYoshi} from './strategies/go-yoshi'; +import {Helm} from './strategies/helm'; +import {Java} from './strategies/java'; import {JavaYoshi} from './strategies/java-yoshi'; import {JavaYoshiMonoRepo} from './strategies/java-yoshi-mono-repo'; import {KRMBlueprint} from './strategies/krm-blueprint'; +import {Maven} from './strategies/maven'; +import {Node} from './strategies/node'; import {OCaml} from './strategies/ocaml'; import {PHP} from './strategies/php'; import {PHPYoshi} from './strategies/php-yoshi'; @@ -28,23 +42,10 @@ import {Rust} from './strategies/rust'; import {Sfdx} from './strategies/sfdx'; import {Simple} from './strategies/simple'; import {TerraformModule} from './strategies/terraform-module'; -import {Helm} from './strategies/helm'; -import {Elixir} from './strategies/elixir'; -import {Dart} from './strategies/dart'; -import {Node} from './strategies/node'; -import {Expo} from './strategies/expo'; -import {GitHub} from './github'; -import {ReleaserConfig} from './manifest'; +import {Strategy} from './strategy'; import {AlwaysBumpPatch} from './versioning-strategies/always-bump-patch'; -import {ServicePackVersioningStrategy} from './versioning-strategies/service-pack'; import {DependencyManifest} from './versioning-strategies/dependency-manifest'; -import {BaseStrategyOptions} from './strategies/base'; -import {DotnetYoshi} from './strategies/dotnet-yoshi'; -import {Java} from './strategies/java'; -import {Maven} from './strategies/maven'; -import {buildVersioningStrategy} from './factories/versioning-strategy-factory'; -import {buildChangelogNotes} from './factories/changelog-notes-factory'; -import {ConfigurationError} from './errors'; +import {ServicePackVersioningStrategy} from './versioning-strategies/service-pack'; export * from './factories/changelog-notes-factory'; export * from './factories/plugin-factory'; @@ -107,6 +108,7 @@ const releasers: Record = { helm: options => new Helm(options), elixir: options => new Elixir(options), dart: options => new Dart(options), + bazel: options => new Bazel(options), }; export async function buildStrategy( diff --git a/src/strategies/bazel.ts b/src/strategies/bazel.ts new file mode 100644 index 000000000..caef10e35 --- /dev/null +++ b/src/strategies/bazel.ts @@ -0,0 +1,51 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {Update} from '../update'; +import {ModuleBazel} from '../updaters/bazel/module-bazel'; +import {Changelog} from '../updaters/changelog'; +import {BaseStrategy, BaseStrategyOptions, BuildUpdatesOptions} from './base'; + +export class Bazel extends BaseStrategy { + readonly versionFile: string; + constructor(options: BaseStrategyOptions) { + super(options); + this.versionFile = options.versionFile ?? 'MODULE.bazel'; + } + protected async buildUpdates( + options: BuildUpdatesOptions + ): Promise { + const updates: Update[] = []; + const version = options.newVersion; + + updates.push({ + path: this.addPath(this.changelogPath), + createIfMissing: true, + updater: new Changelog({ + version, + changelogEntry: options.changelogEntry, + }), + }); + + updates.push({ + path: this.addPath(this.versionFile), + createIfMissing: false, + updater: new ModuleBazel({ + version, + }), + }); + + return updates; + } +} diff --git a/src/updaters/bazel/module-bazel.ts b/src/updaters/bazel/module-bazel.ts new file mode 100644 index 000000000..1f2134c2f --- /dev/null +++ b/src/updaters/bazel/module-bazel.ts @@ -0,0 +1,32 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {DefaultUpdater} from '../default'; + +/** + * Updates a Bazel Module file. + */ +export class ModuleBazel extends DefaultUpdater { + updateContent(content: string): string { + const match = content.match( + /module[\s\S]*?\([\s\S]*?version\s*=\s*(['"])(.*?)\1/m + ); + if (!match) { + return content; + } + const [fullMatch, , version] = match; + const module = fullMatch.replace(version, this.version.toString()); + return content.replace(fullMatch, module); + } +} diff --git a/test/strategies/bazel.ts b/test/strategies/bazel.ts new file mode 100644 index 000000000..7ccb9a5fd --- /dev/null +++ b/test/strategies/bazel.ts @@ -0,0 +1,118 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {expect} from 'chai'; +import {afterEach, beforeEach, describe, it} from 'mocha'; +import * as sinon from 'sinon'; +import {GitHub} from '../../src/github'; +import {Bazel} from '../../src/strategies/bazel'; +import {ModuleBazel} from '../../src/updaters/bazel/module-bazel'; +import {Changelog} from '../../src/updaters/changelog'; +import {TagName} from '../../src/util/tag-name'; +import {Version} from '../../src/version'; +import {assertHasUpdate, buildMockConventionalCommit} from '../helpers'; + +const sandbox = sinon.createSandbox(); + +const COMMITS = [ + ...buildMockConventionalCommit( + 'fix(deps): update dependency com.google.cloud:google-cloud-storage to v1.120.0' + ), + ...buildMockConventionalCommit( + 'fix(deps): update dependency com.google.cloud:google-cloud-spanner to v1.50.0' + ), + ...buildMockConventionalCommit('chore: update common templates'), +]; + +describe('Bazel', () => { + let github: GitHub; + beforeEach(async () => { + github = await GitHub.create({ + owner: 'googleapis', + repo: 'bazel-test-repo', + defaultBranch: 'main', + }); + }); + afterEach(() => { + sandbox.restore(); + }); + describe('buildReleasePullRequest', () => { + it('returns release PR changes with defaultInitialVersion', async () => { + const expectedVersion = '1.0.0'; + const strategy = new Bazel({ + targetBranch: 'main', + github, + component: 'rules_cc', + }); + const latestRelease = undefined; + const release = await strategy.buildReleasePullRequest( + COMMITS, + latestRelease + ); + expect(release!.version?.toString()).to.eql(expectedVersion); + }); + it('returns release PR changes with semver patch bump', async () => { + const expectedVersion = '0.123.5'; + const strategy = new Bazel({ + targetBranch: 'main', + github, + component: 'rules_cc', + }); + const latestRelease = { + tag: new TagName(Version.parse('0.123.4'), 'rules_cc'), + sha: 'abc123', + notes: 'some notes', + }; + const release = await strategy.buildReleasePullRequest( + COMMITS, + latestRelease + ); + expect(release!.version?.toString()).to.eql(expectedVersion); + }); + }); + describe('buildUpdates', () => { + it('builds common files', async () => { + const strategy = new Bazel({ + targetBranch: 'main', + github, + component: 'rules_cc', + }); + const latestRelease = undefined; + const release = await strategy.buildReleasePullRequest( + COMMITS, + latestRelease + ); + const updates = release!.updates; + assertHasUpdate(updates, 'CHANGELOG.md', Changelog); + assertHasUpdate(updates, 'MODULE.bazel', ModuleBazel); + }); + it('allows configuring the version file', async () => { + const strategy = new Bazel({ + targetBranch: 'main', + github, + component: 'rules_cc', + versionFile: 'some-path/MODULE.bazel', + path: 'packages', + }); + const latestRelease = undefined; + const release = await strategy.buildReleasePullRequest( + COMMITS, + latestRelease + ); + const updates = release!.updates; + assertHasUpdate(updates, 'packages/CHANGELOG.md', Changelog); + assertHasUpdate(updates, 'packages/some-path/MODULE.bazel', ModuleBazel); + }); + }); +}); diff --git a/test/updaters/fixtures/MODULE-inline.bazel b/test/updaters/fixtures/MODULE-inline.bazel new file mode 100644 index 000000000..f09c0c294 --- /dev/null +++ b/test/updaters/fixtures/MODULE-inline.bazel @@ -0,0 +1,11 @@ +module(name = "rules_cc", version = "0.0.4") + +bazel_dep(name = "bazel_skylib", version = "1.3.0") +bazel_dep(name = "platforms", version = "0.0.7") + +cc_configure = use_extension("@bazel_tools//tools/cpp:cc_configure.bzl", "cc_configure_extension") +use_repo(cc_configure, "local_config_cc_toolchains") + +register_toolchains("@local_config_cc_toolchains//:all") + +bazel_dep(name = "rules_testing", version = "0.6.0", dev_dependency = True) \ No newline at end of file diff --git a/test/updaters/fixtures/MODULE-missing.bazel b/test/updaters/fixtures/MODULE-missing.bazel new file mode 100644 index 000000000..f1b488e03 --- /dev/null +++ b/test/updaters/fixtures/MODULE-missing.bazel @@ -0,0 +1,9 @@ +bazel_dep(name = "bazel_skylib", version = "1.3.0") +bazel_dep(name = "platforms", version = "0.0.7") + +cc_configure = use_extension("@bazel_tools//tools/cpp:cc_configure.bzl", "cc_configure_extension") +use_repo(cc_configure, "local_config_cc_toolchains") + +register_toolchains("@local_config_cc_toolchains//:all") + +bazel_dep(name = "rules_testing", version = "0.6.0", dev_dependency = True) \ No newline at end of file diff --git a/test/updaters/fixtures/MODULE-order.bazel b/test/updaters/fixtures/MODULE-order.bazel new file mode 100644 index 000000000..476fb3f41 --- /dev/null +++ b/test/updaters/fixtures/MODULE-order.bazel @@ -0,0 +1,11 @@ +bazel_dep(name = "bazel_skylib", version = "1.3.0") +bazel_dep(name = "platforms", version = "0.0.7") + +cc_configure = use_extension("@bazel_tools//tools/cpp:cc_configure.bzl", "cc_configure_extension") +use_repo(cc_configure, "local_config_cc_toolchains") + +module(name = "rules_cc", version = "0.0.4") + +register_toolchains("@local_config_cc_toolchains//:all") + +bazel_dep(name = "rules_testing", version = "0.6.0", dev_dependency = True) \ No newline at end of file diff --git a/test/updaters/fixtures/MODULE.bazel b/test/updaters/fixtures/MODULE.bazel new file mode 100644 index 000000000..8a9b00a14 --- /dev/null +++ b/test/updaters/fixtures/MODULE.bazel @@ -0,0 +1,15 @@ +module( + name = "rules_cc", + version = "0.0.4", + compatibility_level = 1, +) + +bazel_dep(name = "bazel_skylib", version = "1.3.0") +bazel_dep(name = "platforms", version = "0.0.7") + +cc_configure = use_extension("@bazel_tools//tools/cpp:cc_configure.bzl", "cc_configure_extension") +use_repo(cc_configure, "local_config_cc_toolchains") + +register_toolchains("@local_config_cc_toolchains//:all") + +bazel_dep(name = "rules_testing", version = "0.6.0", dev_dependency = True) \ No newline at end of file diff --git a/test/updaters/module-bazel.ts b/test/updaters/module-bazel.ts new file mode 100644 index 000000000..bbfb49679 --- /dev/null +++ b/test/updaters/module-bazel.ts @@ -0,0 +1,75 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {readFileSync} from 'fs'; +import {describe, it} from 'mocha'; +import {resolve} from 'path'; +import * as snapshot from 'snap-shot-it'; +import {ModuleBazel} from '../../src/updaters/bazel/module-bazel'; +import {Version} from '../../src/version'; +import {assert} from 'chai'; + +const fixturesPath = './test/updaters/fixtures'; + +describe('ModuleBazel', () => { + describe('updateContent', () => { + it('updates version in MODULE.bazel file', async () => { + const oldContent = readFileSync( + resolve(fixturesPath, './MODULE.bazel'), + 'utf8' + ).replace(/\r\n/g, '\n'); // required for windows + const version = new ModuleBazel({ + version: Version.parse('0.0.5'), + }); + const newContent = version.updateContent(oldContent); + snapshot(newContent); + }); + + it('updates version when inline', async () => { + const oldContent = readFileSync( + resolve(fixturesPath, './MODULE-inline.bazel'), + 'utf8' + ).replace(/\r\n/g, '\n'); // required for windows + const version = new ModuleBazel({ + version: Version.parse('0.0.5'), + }); + const newContent = version.updateContent(oldContent); + snapshot(newContent); + }); + + it('updates version when ordered improperly', async () => { + const oldContent = readFileSync( + resolve(fixturesPath, './MODULE-order.bazel'), + 'utf8' + ).replace(/\r\n/g, '\n'); // required for windows + const version = new ModuleBazel({ + version: Version.parse('0.0.5'), + }); + const newContent = version.updateContent(oldContent); + snapshot(newContent); + }); + + it('leaves the version file alone if the version is missing', async () => { + const oldContent = readFileSync( + resolve(fixturesPath, './MODULE-missing.bazel'), + 'utf8' + ).replace(/\r\n/g, '\n'); // required for windows + const version = new ModuleBazel({ + version: Version.parse('0.0.5'), + }); + const newContent = version.updateContent(oldContent); + assert(oldContent === newContent); + }); + }); +});