Skip to content

Commit

Permalink
feat: allow date updating in generic strategy
Browse files Browse the repository at this point in the history
Allow updating of dates using the generic updater, by adding a
x-release-please-date or x-release-please-version-date (to update both
version and date) to a file anywhere.

The implementation is still very basic. The date is retrieved by just
checking the current date, but a better approach would be to look at the
timestamp of the previous feat/fix or breaking change for a conventional
commit, I think.

I've added an option --date-format to specify the date format. There are
no assertions with respect to this right now, and the regex matching can
be improved in several ways.

One option I considered was to try to auto-detect the date format, but I
think this is bound to be problematic because there are ambiguities in
date formatting.

Fixes googleapis#1798
  • Loading branch information
jolars committed Nov 20, 2024
1 parent 3e80797 commit 880e565
Show file tree
Hide file tree
Showing 11 changed files with 176 additions and 17 deletions.
2 changes: 2 additions & 0 deletions __snapshots__/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ Options:
--latest-tag-sha Override the detected latest tag SHA[string]
--latest-tag-name Override the detected latest tag name
[string]
--date-format format in strftime format for updating dates
[default: "string"]
--label comma-separated list of labels to add to
from release PR
[default: "autorelease: pending"]
Expand Down
12 changes: 12 additions & 0 deletions __snapshots__/generic.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ public final class Version {
public static String VERSION = "2.3.4";
// {x-release-please-end}
// {x-release-please-start-date}
public static String DATE = "01-12-2023";
// {x-release-please-end}
// {x-release-please-start-version-date}
public static String NEW_DATE = "01-12-2023";
public static String NEW_VERSION = "2.3.4";
// {x-release-please-end}
// {x-release-please-start-major}
public static String MAJOR = "2";
// {x-release-please-end}
Expand All @@ -37,6 +46,9 @@ public final class 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}
public static String RELEASE_DATE = "01-12-2023"; // {x-release-please-date}
public static String RELEASE_INFO = "v2.3.4 01-12-2023"; // {x-release-please-version-date}
}
`
5 changes: 5 additions & 0 deletions schemas/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@
"description": "Customize the separator between the component and version in the GitHub tag.",
"type": "string"
},
"date-format": {
"description": "Date format given as a strftime expression for the generic strategy.",
"type": "string"
},
"extra-files": {
"description": "Specify extra generic files to replace versions.",
"type": "array",
Expand Down Expand Up @@ -476,6 +480,7 @@
"separate-pull-requests": true,
"always-update": true,
"tag-separator": true,
"date-format": true,
"extra-files": true,
"version-file": true,
"snapshot-label": true,
Expand Down
4 changes: 4 additions & 0 deletions src/bin/release-please.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,10 @@ function pullRequestStrategyOptions(yargs: yargs.Argv): yargs.Argv {
describe: 'Override the detected latest tag name',
type: 'string',
})
.option('date-format', {
describe: 'format in strftime format for updating dates',
default: 'string',
})
.middleware(_argv => {
const argv = _argv as CreatePullRequestArgs;
Expand Down
5 changes: 5 additions & 0 deletions src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export interface ReleaserConfig {
releaseLabels?: string[];
extraLabels?: string[];
initialVersion?: string;
dateFormat?: string;

// Changelog options
changelogSections?: ChangelogSection[];
Expand Down Expand Up @@ -183,6 +184,7 @@ interface ReleaserConfigJson {
'skip-snapshot'?: boolean; // Java-only
'initial-version'?: string;
'exclude-paths'?: string[]; // manifest-only
'date-format'?: string;
}

export interface ManifestOptions {
Expand All @@ -207,6 +209,7 @@ export interface ManifestOptions {
releaseSearchDepth?: number;
commitSearchDepth?: number;
logger?: Logger;
dateFormat?: string;
}

export interface ReleaserPackageConfig extends ReleaserConfigJson {
Expand Down Expand Up @@ -1397,6 +1400,7 @@ function extractReleaserConfig(
skipSnapshot: config['skip-snapshot'],
initialVersion: config['initial-version'],
excludePaths: config['exclude-paths'],
dateFormat: config['date-format'],
};
}

Expand Down Expand Up @@ -1755,6 +1759,7 @@ function mergeReleaserConfig(
initialVersion: pathConfig.initialVersion ?? defaultConfig.initialVersion,
extraLabels: pathConfig.extraLabels ?? defaultConfig.extraLabels,
excludePaths: pathConfig.excludePaths ?? defaultConfig.excludePaths,
dateFormat: pathConfig.dateFormat ?? defaultConfig.dateFormat,
};
}

Expand Down
30 changes: 22 additions & 8 deletions src/strategies/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export interface BaseStrategyOptions {
logger?: Logger;
initialVersion?: string;
extraLabels?: string[];
dateFormat?: string;
}

/**
Expand Down Expand Up @@ -113,6 +114,7 @@ export abstract class BaseStrategy implements Strategy {
readonly componentNoSpace?: boolean;
readonly extraFiles: ExtraFile[];
readonly extraLabels: string[];
protected dateFormat: string;

readonly changelogNotes: ChangelogNotes;

Expand Down Expand Up @@ -148,6 +150,7 @@ export abstract class BaseStrategy implements Strategy {
this.extraFiles = options.extraFiles || [];
this.initialVersion = options.initialVersion;
this.extraLabels = options.extraLabels || [];
this.dateFormat = options.dateFormat || '%Y-%m-%d';
}

/**
Expand Down Expand Up @@ -330,7 +333,13 @@ export abstract class BaseStrategy implements Strategy {
commits: conventionalCommits,
});
const updatesWithExtras = mergeUpdates(
updates.concat(...(await this.extraFileUpdates(newVersion, versionsMap)))
updates.concat(
...(await this.extraFileUpdates(
newVersion,
versionsMap,
this.dateFormat
))
)
);
const pullRequestBody = await this.buildPullRequestBody(
component,
Expand Down Expand Up @@ -390,7 +399,8 @@ export abstract class BaseStrategy implements Strategy {

protected async extraFileUpdates(
version: Version,
versionsMap: VersionsMap
versionsMap: VersionsMap,
dateFormat: string
): Promise<Update[]> {
const extraFileUpdates: Update[] = [];
for (const extraFile of this.extraFiles) {
Expand All @@ -402,7 +412,11 @@ export abstract class BaseStrategy implements Strategy {
extraFileUpdates.push({
path: this.addPath(path),
createIfMissing: false,
updater: new Generic({version, versionsMap}),
updater: new Generic({
version,
versionsMap,
dateFormat: dateFormat,
}),
});
break;
case 'json':
Expand Down Expand Up @@ -454,7 +468,7 @@ export abstract class BaseStrategy implements Strategy {
createIfMissing: false,
updater: new CompositeUpdater(
new GenericJson('$.version', version),
new Generic({version, versionsMap})
new Generic({version, versionsMap, dateFormat: dateFormat})
),
});
} else if (extraFile.endsWith('.yaml') || extraFile.endsWith('.yml')) {
Expand All @@ -463,7 +477,7 @@ export abstract class BaseStrategy implements Strategy {
createIfMissing: false,
updater: new CompositeUpdater(
new GenericYaml('$.version', version),
new Generic({version, versionsMap})
new Generic({version, versionsMap, dateFormat: dateFormat})
),
});
} else if (extraFile.endsWith('.toml')) {
Expand All @@ -472,7 +486,7 @@ export abstract class BaseStrategy implements Strategy {
createIfMissing: false,
updater: new CompositeUpdater(
new GenericToml('$.version', version),
new Generic({version, versionsMap})
new Generic({version, versionsMap, dateFormat: dateFormat})
),
});
} else if (extraFile.endsWith('.xml')) {
Expand All @@ -482,14 +496,14 @@ export abstract class BaseStrategy implements Strategy {
updater: new CompositeUpdater(
// Updates "version" element that is a child of the root element.
new GenericXml('/*/version', version),
new Generic({version, versionsMap})
new Generic({version, versionsMap, dateFormat: dateFormat})
),
});
} else {
extraFileUpdates.push({
path: this.addPath(extraFile),
createIfMissing: false,
updater: new Generic({version, versionsMap}),
updater: new Generic({version, versionsMap, dateFormat: dateFormat}),
});
}
}
Expand Down
8 changes: 7 additions & 1 deletion src/strategies/java.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,13 @@ export class Java extends BaseStrategy {
commits: [],
});
const updatesWithExtras = mergeUpdates(
updates.concat(...(await this.extraFileUpdates(newVersion, versionsMap)))
updates.concat(
...(await this.extraFileUpdates(
newVersion,
versionsMap,
this.dateFormat
))
)
);
return {
title: pullRequestTitle,
Expand Down
111 changes: 103 additions & 8 deletions src/updaters/generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,19 @@ 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)/;
/x-release-please-(?<scope>major|minor|patch|version-date|version|date)/;
const BLOCK_START_REGEX =
/x-release-please-start-(?<scope>major|minor|patch|version)/;
/x-release-please-start-(?<scope>major|minor|patch|version-date|version|date)/;
const BLOCK_END_REGEX = /x-release-please-end/;
const DATE_FORMAT_REGEX = /%[Ymd]/g;

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

/**
* Options for the Generic updater.
Expand All @@ -34,6 +41,8 @@ export interface GenericUpdateOptions extends UpdateOptions {
inlineUpdateRegex?: RegExp;
blockStartRegex?: RegExp;
blockEndRegex?: RegExp;
date?: Date;
dateFormat?: string;
}

/**
Expand All @@ -52,24 +61,32 @@ export interface GenericUpdateOptions extends UpdateOptions {
* 4. `x-release-please-patch` if this string is found on the line,
* then replace an integer looking value with the next version's
* patch
* 5. `x-release-please-date` if this string is found on the line,
* then replace the date with the date of the last commit
* 6. `x-release-please-version-date` if this string is found on the line,
* then replace the both date and version
*
* 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
* with `x-release-please-start-<major|minor|patch|version-date>` to replace
* single numbers
*/
export class Generic extends DefaultUpdater {
private readonly inlineUpdateRegex: RegExp;
private readonly blockStartRegex: RegExp;
private readonly blockEndRegex: RegExp;
private readonly date: Date;
private readonly dateFormat: string;

constructor(options: GenericUpdateOptions) {
super(options);

this.inlineUpdateRegex = options.inlineUpdateRegex ?? INLINE_UPDATE_REGEX;
this.blockStartRegex = options.blockStartRegex ?? BLOCK_START_REGEX;
this.blockEndRegex = options.blockEndRegex ?? BLOCK_END_REGEX;
this.date = options.date ?? new Date();
this.dateFormat = options.dateFormat ?? '%Y-%m-%d';
}

/**
Expand All @@ -88,8 +105,33 @@ export class Generic extends DefaultUpdater {
const newLines: string[] = [];
let blockScope: BlockScope | undefined;

function replaceVersion(line: string, scope: BlockScope, version: Version) {
function replaceVersion(
line: string,
scope: BlockScope,
version: Version,
date: Date,
dateFormat: string
) {
const dateRegex = createDateRegex(dateFormat);
const formattedDate = formatDate(dateFormat, date);

switch (scope) {
case 'date':
if (isValidDate(formattedDate, dateFormat)) {
newLines.push(line.replace(dateRegex, formattedDate));
} else {
logger.warn(`Invalid date format: ${formattedDate}`);
newLines.push(line);
}
return;
case 'version-date':
if (isValidDate(formattedDate, dateFormat)) {
line = line.replace(dateRegex, formattedDate);
} else {
logger.warn(`Invalid date format: ${formattedDate}`);
}
newLines.push(line.replace(VERSION_REGEX, version.toString()));
return;
case 'major':
newLines.push(line.replace(SINGLE_VERSION_REGEX, `${version.major}`));
return;
Expand All @@ -115,11 +157,19 @@ export class Generic extends DefaultUpdater {
replaceVersion(
line,
(match.groups?.scope || 'version') as BlockScope,
this.version
this.version,
this.date,
this.dateFormat
);
} else if (blockScope) {
// in a block, so try to replace versions
replaceVersion(line, blockScope, this.version);
replaceVersion(
line,
blockScope,
this.version,
this.date,
this.dateFormat
);
if (line.match(this.blockEndRegex)) {
blockScope = undefined;
}
Expand All @@ -140,3 +190,48 @@ export class Generic extends DefaultUpdater {
return newLines.join('\n');
}
}

function createDateRegex(format: string): RegExp {
const regexString = format.replace(DATE_FORMAT_REGEX, match => {
switch (match) {
case '%Y':
return '(\\d{4})';
case '%m':
return '(\\d{2})';
case '%d':
return '(\\d{2})';
default:
return match;
}
});
return new RegExp(regexString);
}

function formatDate(format: string, date: Date): string {
return format.replace(DATE_FORMAT_REGEX, match => {
switch (match) {
case '%Y':
return date.getFullYear().toString();
case '%m':
return ('0' + (date.getMonth() + 1)).slice(-2);
case '%d':
return ('0' + date.getDate()).slice(-2);
default:
return match;
}
});
}

function isValidDate(dateString: string, format: string): boolean {
const dateParts = dateString.match(/\d+/g);
if (!dateParts) return false;

const year = parseInt(dateParts[format.indexOf('%Y') / 3], 10);
const month = parseInt(dateParts[format.indexOf('%m') / 3], 10);
const day = parseInt(dateParts[format.indexOf('%d') / 3], 10);

if (year < 1 || month < 1 || month > 12 || day < 1 || day > 31) return false;

const daysInMonth = new Date(year, month, 0).getDate();
return day <= daysInMonth;
}
Loading

0 comments on commit 880e565

Please sign in to comment.