diff --git a/__snapshots__/cli.js b/__snapshots__/cli.js index 6b74353e2..fc4b4428c 100644 --- a/__snapshots__/cli.js +++ b/__snapshots__/cli.js @@ -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"] diff --git a/__snapshots__/generic.js b/__snapshots__/generic.js index 58cc5a460..583803b77 100644 --- a/__snapshots__/generic.js +++ b/__snapshots__/generic.js @@ -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} @@ -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} } ` diff --git a/schemas/config.json b/schemas/config.json index 9d883effa..100c16ac9 100644 --- a/schemas/config.json +++ b/schemas/config.json @@ -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", @@ -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, diff --git a/src/bin/release-please.ts b/src/bin/release-please.ts index b3240fc69..80f06e9a9 100644 --- a/src/bin/release-please.ts +++ b/src/bin/release-please.ts @@ -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; diff --git a/src/manifest.ts b/src/manifest.ts index 390154573..fef1006c8 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -122,6 +122,7 @@ export interface ReleaserConfig { releaseLabels?: string[]; extraLabels?: string[]; initialVersion?: string; + dateFormat?: string; // Changelog options changelogSections?: ChangelogSection[]; @@ -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 { @@ -207,6 +209,7 @@ export interface ManifestOptions { releaseSearchDepth?: number; commitSearchDepth?: number; logger?: Logger; + dateFormat?: string; } export interface ReleaserPackageConfig extends ReleaserConfigJson { @@ -1397,6 +1400,7 @@ function extractReleaserConfig( skipSnapshot: config['skip-snapshot'], initialVersion: config['initial-version'], excludePaths: config['exclude-paths'], + dateFormat: config['date-format'], }; } @@ -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, }; } diff --git a/src/strategies/base.ts b/src/strategies/base.ts index 00495e426..bbc96ca90 100644 --- a/src/strategies/base.ts +++ b/src/strategies/base.ts @@ -84,6 +84,7 @@ export interface BaseStrategyOptions { logger?: Logger; initialVersion?: string; extraLabels?: string[]; + dateFormat?: string; } /** @@ -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; @@ -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'; } /** @@ -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, @@ -390,7 +399,8 @@ export abstract class BaseStrategy implements Strategy { protected async extraFileUpdates( version: Version, - versionsMap: VersionsMap + versionsMap: VersionsMap, + dateFormat: string ): Promise { const extraFileUpdates: Update[] = []; for (const extraFile of this.extraFiles) { @@ -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': @@ -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')) { @@ -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')) { @@ -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')) { @@ -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}), }); } } diff --git a/src/strategies/java.ts b/src/strategies/java.ts index c4a2e1696..6fca52287 100644 --- a/src/strategies/java.ts +++ b/src/strategies/java.ts @@ -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, diff --git a/src/updaters/generic.ts b/src/updaters/generic.ts index 42cc268c5..c849c4d5a 100644 --- a/src/updaters/generic.ts +++ b/src/updaters/generic.ts @@ -20,12 +20,19 @@ const VERSION_REGEX = /(?\d+)\.(?\d+)\.(?\d+)(-(?[\w.]+))?(\+(?[-\w.]+))?/; const SINGLE_VERSION_REGEX = /\b\d+\b/; const INLINE_UPDATE_REGEX = - /x-release-please-(?major|minor|patch|version)/; + /x-release-please-(?major|minor|patch|version-date|version|date)/; const BLOCK_START_REGEX = - /x-release-please-start-(?major|minor|patch|version)/; + /x-release-please-start-(?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. @@ -34,6 +41,8 @@ export interface GenericUpdateOptions extends UpdateOptions { inlineUpdateRegex?: RegExp; blockStartRegex?: RegExp; blockEndRegex?: RegExp; + date?: Date; + dateFormat?: string; } /** @@ -52,17 +61,23 @@ 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-` to replace single - * numbers + * with `x-release-please-start-` 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); @@ -70,6 +85,8 @@ export class Generic extends DefaultUpdater { 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'; } /** @@ -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; @@ -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; } @@ -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; +} diff --git a/src/updaters/release-please-config.ts b/src/updaters/release-please-config.ts index 4cea722fa..20a3869d6 100644 --- a/src/updaters/release-please-config.ts +++ b/src/updaters/release-please-config.ts @@ -82,6 +82,7 @@ function releaserConfigToJsonConfig( 'extra-files': config.extraFiles, 'version-file': config.versionFile, 'snapshot-label': config.snapshotLabels?.join(','), // Java-only + 'date-format': config.dateFormat, }; return jsonConfig; } diff --git a/test/updaters/fixtures/Version.java b/test/updaters/fixtures/Version.java index ffee3d46f..aab0e8be4 100644 --- a/test/updaters/fixtures/Version.java +++ b/test/updaters/fixtures/Version.java @@ -20,6 +20,15 @@ public final class Version { public static String VERSION = "1.2.3-SNAPSHOT"; // {x-release-please-end} + // {x-release-please-start-date} + public static String DATE = "10-09-2100"; + // {x-release-please-end} + + // {x-release-please-start-version-date} + public static String NEW_DATE = "01-01-0100"; + public static String NEW_VERSION = "3.2.0-SNAPSHOT"; + // {x-release-please-end} + // {x-release-please-start-major} public static String MAJOR = "1"; // {x-release-please-end} @@ -36,4 +45,7 @@ public final class Version { public static String INLINE_MAJOR = "1"; // {x-release-please-major} public static String INLINE_MINOR = "2"; // {x-release-please-minor} public static String INLINE_PATCH = "3"; // {x-release-please-patch} + + public static String RELEASE_DATE = "11-12-2014"; // {x-release-please-date} + public static String RELEASE_INFO = "v1.2.3 11-12-2014"; // {x-release-please-version-date} } diff --git a/test/updaters/generic.ts b/test/updaters/generic.ts index 49ba68ce9..e7e12c791 100644 --- a/test/updaters/generic.ts +++ b/test/updaters/generic.ts @@ -29,9 +29,12 @@ describe('Generic', () => { 'utf8' ).replace(/\r\n/g, '\n'); const versions = new Map(); + const currentDate = new Date(Date.parse('2023-12-01')); const pom = new Generic({ versionsMap: versions, version: Version.parse('v2.3.4'), + date: currentDate, + dateFormat: '%d-%m-%Y', }); const newContent = pom.updateContent(oldContent); snapshot(newContent);