Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce generic updater #1157

Merged
merged 10 commits into from
Dec 29, 2021
42 changes: 42 additions & 0 deletions __snapshots__/generic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
exports['Generic updateContent updates generic version markers 1'] = `
/*
* Copyright 2021 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
*
* https://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.
*/
package com.google.cloud.example;

public final class Version {
// {x-release-please-start-version}
bcoe marked this conversation as resolved.
Show resolved Hide resolved
public static String VERSION = "2.3.4";
// {x-release-please-end}

// {x-release-please-start-major}
public static String MAJOR = "2";
// {x-release-please-end}

// {x-release-please-start-minor}
public static String MINOR = "3";
// {x-release-please-end}

// {x-release-please-start-patch}
public static String PATCH = "4";
// {x-release-please-end}

public static String INLINE_VERSION = "2.3.4"; // {x-release-please-version}
public static String INLINE_MAJOR = "2"; // {x-release-please-major}
public static String INLINE_MINOR = "3"; // {x-release-please-minor}
public static String INLINE_PATCH = "4"; // {x-release-please-patch}
}

`
11 changes: 2 additions & 9 deletions src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ type Releasers = Record<string, ReleaseBuilder>;
const releasers: Releasers = {
go: options => new Go(options),
'go-yoshi': options => new GoYoshi(options),
'java-yoshi': options => new JavaYoshi(options),
'krm-blueprint': options => new KRMBlueprint(options),
node: options => new Node(options),
ocaml: options => new OCaml(options),
Expand Down Expand Up @@ -140,6 +141,7 @@ export async function buildStrategy(
includeComponentInTag: options.includeComponentInTag,
changelogNotes,
pullRequestTitlePattern: options.pullRequestTitlePattern,
extraFiles: options.extraFiles,
};
switch (options.releaseType) {
case 'ruby': {
Expand All @@ -154,23 +156,15 @@ export async function buildStrategy(
versionFile: options.versionFile,
});
}
case 'java-yoshi': {
return new JavaYoshi({
...strategyOptions,
extraFiles: options.extraFiles,
});
}
case 'java-backport': {
return new JavaYoshi({
...strategyOptions,
extraFiles: options.extraFiles,
versioningStrategy: new AlwaysBumpPatch(),
});
}
case 'java-bom': {
return new JavaYoshi({
...strategyOptions,
extraFiles: options.extraFiles,
versioningStrategy: new DependencyManifest({
bumpMinorPreMajor: options.bumpMinorPreMajor,
bumpPatchForMinorPreMajor: options.bumpPatchForMinorPreMajor,
Expand All @@ -180,7 +174,6 @@ export async function buildStrategy(
case 'java-lts': {
return new JavaYoshi({
...strategyOptions,
extraFiles: options.extraFiles,
versioningStrategy: new ServicePackVersioningStrategy(),
});
}
Expand Down
27 changes: 4 additions & 23 deletions src/plugins/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {PullRequestTitle} from '../util/pull-request-title';
import {PullRequestBody, ReleaseData} from '../util/pull-request-body';
import {BranchName} from '../util/branch-name';
import {Update} from '../update';
import {CompositeUpdater} from '../updaters/composite';
import {mergeUpdates} from '../updaters/composite';

/**
* This plugin merges multiple pull requests into a single
Expand All @@ -38,37 +38,18 @@ export class Merge extends ManifestPlugin {
return candidates;
}

const updatesByPath: Record<string, Update[]> = {};
const releaseData: ReleaseData[] = [];
const labels = new Set<string>();
let rawUpdates: Update[] = [];
for (const candidate of candidates) {
const pullRequest = candidate.pullRequest;
for (const update of pullRequest.updates) {
if (updatesByPath[update.path]) {
updatesByPath[update.path].push(update);
} else {
updatesByPath[update.path] = [update];
}
}
rawUpdates = rawUpdates.concat(...pullRequest.updates);
for (const label of pullRequest.labels) {
labels.add(label);
}
releaseData.push(...pullRequest.body.releaseData);
}

const updates: Update[] = [];
for (const path in updatesByPath) {
const update = updatesByPath[path];
const updaters = update.map(u => u.updater);
updates.push({
path,
createIfMissing: update[0].createIfMissing,
updater:
updaters.length === 1
? updaters[0]
: new CompositeUpdater(...updaters),
});
}
const updates = mergeUpdates(rawUpdates);

const pullRequest = {
title: PullRequestTitle.ofTargetBranch(
Expand Down
21 changes: 20 additions & 1 deletion src/strategies/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import {PullRequestTitle} from '../util/pull-request-title';
import {BranchName} from '../util/branch-name';
import {PullRequestBody} from '../util/pull-request-body';
import {PullRequest} from '../pull-request';
import {mergeUpdates} from '../updaters/composite';
import {Generic} from '../updaters/generic';

const DEFAULT_CHANGELOG_PATH = 'CHANGELOG.md';

Expand Down Expand Up @@ -63,6 +65,7 @@ export interface BaseStrategyOptions {
changelogNotes?: ChangelogNotes;
includeComponentInTag?: boolean;
pullRequestTitlePattern?: string;
extraFiles?: string[];
}

/**
Expand All @@ -83,6 +86,7 @@ export abstract class BaseStrategy implements Strategy {
private releaseAs?: string;
private includeComponentInTag: boolean;
private pullRequestTitlePattern?: string;
readonly extraFiles: string[];

readonly changelogNotes: ChangelogNotes;

Expand All @@ -108,6 +112,7 @@ export abstract class BaseStrategy implements Strategy {
options.changelogNotes || new DefaultChangelogNotes(options);
this.includeComponentInTag = options.includeComponentInTag ?? true;
this.pullRequestTitlePattern = options.pullRequestTitlePattern;
this.extraFiles = options.extraFiles || [];
}

/**
Expand Down Expand Up @@ -239,6 +244,9 @@ export abstract class BaseStrategy implements Strategy {
versionsMap,
latestVersion: latestRelease?.tag.version,
});
const updatesWithExtras = mergeUpdates(
updates.concat(...this.extraFileUpdates(newVersion))
);
const pullRequestBody = new PullRequestBody([
{
component,
Expand All @@ -250,14 +258,25 @@ export abstract class BaseStrategy implements Strategy {
return {
title: pullRequestTitle,
body: pullRequestBody,
updates,
updates: updatesWithExtras,
labels,
headRefName: branchName.toString(),
version: newVersion,
draft: draft ?? false,
};
}

private extraFileUpdates(version: Version): Update[] {
const genericUpdater = new Generic({version});
return this.extraFiles.map(path => {
return {
path,
createIfMissing: false,
updater: genericUpdater,
};
});
}

protected changelogEmpty(changelogEntry: string): boolean {
return changelogEntry.split('\n').length <= 1;
}
Expand Down
8 changes: 1 addition & 7 deletions src/strategies/java-yoshi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,17 @@ const CHANGELOG_SECTIONS = [
{type: 'ci', section: 'Continuous Integration', hidden: true},
];

interface JavaStrategyOptions extends BaseStrategyOptions {
extraFiles?: string[];
}

export class JavaYoshi extends BaseStrategy {
readonly extraFiles: string[];
private versionsContent?: GitHubFileContents;
private snapshotVersioning: VersioningStrategy;

constructor(options: JavaStrategyOptions) {
constructor(options: BaseStrategyOptions) {
options.changelogSections = options.changelogSections ?? CHANGELOG_SECTIONS;
// wrap the configured versioning strategy with snapshotting
const parentVersioningStrategy =
options.versioningStrategy || new DefaultVersioningStrategy();
options.versioningStrategy = new JavaSnapshot(parentVersioningStrategy);
super(options);
this.extraFiles = options.extraFiles || [];
this.snapshotVersioning = new JavaAddSnapshot(parentVersioningStrategy);
}

Expand Down
28 changes: 26 additions & 2 deletions src/updaters/composite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import {Updater} from '../update';
import {Updater, Update} from '../update';

/**
* The CompositeUpdater chains 0...n updaters and updates
* the content in order.
*/
export class CompositeUpdater implements Updater {
updaters: Updater[];
readonly updaters: Updater[];
/**
* Instantiate a new CompositeUpdater
* @param {Updater[]} updaters The updaters to chain together
Expand All @@ -40,3 +40,27 @@ export class CompositeUpdater implements Updater {
return content || '';
}
}

export function mergeUpdates(updates: Update[]): Update[] {
const updatesByPath: Record<string, Update[]> = {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to have Record<Update['path'], Update[]>?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would probably work in case update's path's type changes, but I think for readability, string might be easier to read.

for (const update of updates) {
if (updatesByPath[update.path]) {
updatesByPath[update.path].push(update);
} else {
updatesByPath[update.path] = [update];
}
}

const newUpdates: Update[] = [];
for (const path in updatesByPath) {
const update = updatesByPath[path];
const updaters = update.map(u => u.updater);
newUpdates.push({
path,
createIfMissing: update[0].createIfMissing,
updater:
updaters.length === 1 ? updaters[0] : new CompositeUpdater(...updaters),
});
}
return newUpdates;
}
118 changes: 118 additions & 0 deletions src/updaters/generic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright 2021 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';
import {Version} from '../version';
import {logger} from '../util/logger';

const VERSION_REGEX =
/(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(-(?<preRelease>[\w.]+))?(\+(?<build>[-\w.]+))?/;
const SINGLE_VERSION_REGEX = /\b\d+\b/;
const INLINE_UPDATE_REGEX =
/x-release-please-(?<scope>major|minor|patch|version)/;
const BLOCK_START_REGEX =
/x-release-please-start-(?<scope>major|minor|patch|version)/;
const BLOCK_END_REGEX = /x-release-please-end/;

type BlockScope = 'major' | 'minor' | 'patch' | 'version';

/**
* The Generic updater looks for well known patterns and replaces
* content. The well known patterns are:
*
* 1. `x-release-please-version` if this string is found on the line,
* then replace a semver-looking string on that line with the next
* version
* 2. `x-release-please-major` if this string is found on the line,
* then replace an integer looking value with the the next version's
* major
* 3. `x-release-please-minor` if this string is found on the line,
* then replace an integer looking value with the the next version's
* minor
* 4. `x-release-please-patch` if this string is found on the line,
* then replace an integer looking value with the the next version's
* patch
*
* You can also use a block-based replacement. Content between the
* opening `x-release-please-start-version` and `x-release-please-end` will
* be considered for version replacement. You can also open these blocks
* with `x-release-please-start-<major|minor|patch>` to replace single
* numbers
*/
export class Generic extends DefaultUpdater {
/**
* Given initial file contents, return updated contents.
* @param {string} content The initial content
* @returns {string} The updated content
*/
updateContent(content: string | undefined): string {
if (!content) {
return '';
}

const newLines: string[] = [];
let blockScope: BlockScope | undefined;

function replaceVersion(line: string, scope: BlockScope, version: Version) {
switch (scope) {
case 'major':
newLines.push(line.replace(SINGLE_VERSION_REGEX, `${version.major}`));
return;
case 'minor':
newLines.push(line.replace(SINGLE_VERSION_REGEX, `${version.minor}`));
return;
case 'patch':
newLines.push(line.replace(SINGLE_VERSION_REGEX, `${version.patch}`));
return;
case 'version':
newLines.push(line.replace(VERSION_REGEX, version.toString()));
return;
default:
logger.warn(`unknown block scope: ${scope}`);
newLines.push(line);
}
}

content.split(/\r?\n/).forEach(line => {
let match = line.match(INLINE_UPDATE_REGEX);
if (match) {
// replace inline versions
replaceVersion(
line,
(match.groups?.scope || 'version') as BlockScope,
this.version
);
} else if (blockScope) {
// in a block, so try to replace versions
replaceVersion(line, blockScope, this.version);
if (line.match(BLOCK_END_REGEX)) {
blockScope = undefined;
}
} else {
// look for block start line
match = line.match(BLOCK_START_REGEX);
if (match) {
if (match.groups?.scope) {
blockScope = match.groups.scope as BlockScope;
} else {
blockScope = 'version';
}
}
newLines.push(line);
}
});

return newLines.join('\n');
}
}
Loading