diff --git a/__snapshots__/cli.js b/__snapshots__/cli.js index 60389f1fa..216336746 100644 --- a/__snapshots__/cli.js +++ b/__snapshots__/cli.js @@ -190,7 +190,7 @@ Options: the first major release [boolean] [default: false] --prerelease-type type of the prerelease, e.g., alpha [string] - --extra-files extra files for the strategy to consider + --extra-files extra files for the strategy to update [string] --version-file path to version file to update, e.g., version.rb [string] diff --git a/docs/cli.md b/docs/cli.md index d6a6abedd..b527d6c0c 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -64,7 +64,7 @@ Extra options: | `--pull-request-title-pattern` | `string` | Override the pull request title pattern. Defaults to `chore${scope}: release${component} ${version}` | | `--pull-request-header` | `string` | Override the pull request header. Defaults to `:robot: I have created a release *beep* *boop*` | | `--pull-request-footer` | `string` | Override the pull request footer. Defaults to `This PR was generated with Release Please. See documentation.` | -| `--extra-files` | `string[]` | Extra file paths for the release strategy to consider | +| `--extra-files` | `string[]` | Extra file paths for the release strategy to update | | `--version-file` | `string` | Ruby only. Path to the `version.rb` file | ## Creating/updating release PRs @@ -115,7 +115,7 @@ need to specify your release options: | `--pull-request-header` | `string` | Override the pull request header. Defaults to `:robot: I have created a release *beep* *boop*` | | `--pull-request-footer` | `string` | Override the pull request footer. Defaults to `This PR was generated with Release Please. See documentation.` | | `--signoff` | string | Add [`Signed-off-by`](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---signoff) line at the end of the commit log message using the user and email provided. (format "Name \") | -| `--extra-files` | `string[]` | Extra file paths for the release strategy to consider | +| `--extra-files` | `string[]` | Extra file paths for the release strategy to update | | `--version-file` | `string` | Ruby only. Path to the `version.rb` file | | `--skip-labeling` | `boolean` | If set, labels will not be applied to pull requests | | `--include-v-in-tags` | `boolean` | Include "v" in tag versions. Defaults to `true`. | diff --git a/docs/manifest-releaser.md b/docs/manifest-releaser.md index cfc3e0248..b651170b4 100644 --- a/docs/manifest-releaser.md +++ b/docs/manifest-releaser.md @@ -267,6 +267,8 @@ defaults (those are documented in comments) "release-type": "node", // exclude commits from that path from processing "exclude-paths": ["path/to/myPyPkgA"] + // include commits from that path in processing + "additional-paths": ["path/to/externalPkgB"] }, // path segment should be relative to repository root diff --git a/schemas/config.json b/schemas/config.json index 2477278a5..c1607153e 100644 --- a/schemas/config.json +++ b/schemas/config.json @@ -199,6 +199,13 @@ "type": "string" } }, + "additional-paths": { + "description": "Path of commits, from outside the package, to be included in parsing.", + "type": "array", + "items": { + "type": "string" + } + }, "version-file": { "description": "Path to the specialize version file. Used by `ruby` and `simple` strategies.", "type": "string" @@ -443,6 +450,7 @@ "version-file": true, "snapshot-label": true, "initial-version": true, - "exclude-paths": true + "exclude-paths": true, + "additional-paths": true } } diff --git a/src/bin/release-please.ts b/src/bin/release-please.ts index 4baf2c626..55ae2d6b2 100644 --- a/src/bin/release-please.ts +++ b/src/bin/release-please.ts @@ -282,7 +282,7 @@ function pullRequestStrategyOptions(yargs: yargs.Argv): yargs.Argv { type: 'string', }) .option('extra-files', { - describe: 'extra files for the strategy to consider', + describe: 'extra files for the strategy to update', type: 'string', coerce(arg?: string) { if (arg) { diff --git a/src/manifest.ts b/src/manifest.ts index 10ac85748..f8ef43e0e 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -137,6 +137,7 @@ export interface ReleaserConfig { skipSnapshot?: boolean; // Manifest only excludePaths?: string[]; + additionalPaths?: string[]; } export interface CandidateReleasePullRequest { @@ -183,6 +184,7 @@ interface ReleaserConfigJson { 'skip-snapshot'?: boolean; // Java-only 'initial-version'?: string; 'exclude-paths'?: string[]; // manifest-only + 'additional-paths'?: string[]; // manifest-only } export interface ManifestOptions { @@ -658,7 +660,12 @@ export class Manifest { this.logger.info(`Splitting ${commits.length} commits by path`); const cs = new CommitSplit({ includeEmpty: true, - packagePaths: Object.keys(this.repositoryConfig), + packagePaths: Object.fromEntries( + Object.entries(this.repositoryConfig).map(([path, config]) => [ + path, + config.additionalPaths || [], + ]) + ), }); const splitCommits = cs.split(commits); @@ -1372,6 +1379,7 @@ function extractReleaserConfig( skipSnapshot: config['skip-snapshot'], initialVersion: config['initial-version'], excludePaths: config['exclude-paths'], + additionalPaths: config['additional-paths'], signoff: config['signoff'], }; } @@ -1727,6 +1735,8 @@ function mergeReleaserConfig( initialVersion: pathConfig.initialVersion ?? defaultConfig.initialVersion, extraLabels: pathConfig.extraLabels ?? defaultConfig.extraLabels, excludePaths: pathConfig.excludePaths ?? defaultConfig.excludePaths, + additionalPaths: + pathConfig.additionalPaths ?? defaultConfig.additionalPaths, }; } diff --git a/src/util/commit-split.ts b/src/util/commit-split.ts index c8a2d5648..f9ab6581c 100644 --- a/src/util/commit-split.ts +++ b/src/util/commit-split.ts @@ -14,7 +14,7 @@ import {Commit} from '../commit'; import {ROOT_PROJECT_PATH} from '../manifest'; -import {normalizePaths} from './commit-utils'; +import {normalizePath, normalizePaths} from './commit-utils'; export interface CommitSplitOptions { // Include empty git commits: each empty commit is included @@ -39,7 +39,7 @@ export interface CommitSplitOptions { // // NOTE: GitHub API always returns paths using the `/` separator, regardless // of what platform the client code is running on - packagePaths?: string[]; + packagePaths?: Record; } /** @@ -50,19 +50,24 @@ export interface CommitSplitOptions { */ export class CommitSplit { includeEmpty: boolean; - packagePaths?: string[]; + packagePaths?: Record; constructor(opts?: CommitSplitOptions) { opts = opts || {}; this.includeEmpty = !!opts.includeEmpty; if (opts.packagePaths) { - const paths: string[] = normalizePaths(opts.packagePaths); - this.packagePaths = paths - .filter(path => { - // The special "." path, representing the root of the module, should be - // ignored by commit-split as it is assigned all commits in manifest.ts - return path !== ROOT_PROJECT_PATH; - }) - .sort((a, b) => b.length - a.length); // sort by longest paths first + this.packagePaths = Object.fromEntries( + Object.entries(opts.packagePaths) + .map(([path, additionalPaths]) => [ + normalizePath(path), + normalizePaths(additionalPaths), + ]) + .filter(([path]) => { + // The special "." path, representing the root of the module, should be + // ignored by commit-split as it is assigned all commits in manifest.ts + return path !== ROOT_PROJECT_PATH; + }) + .sort(([a], [b]) => b.length - a.length) // sort by longest paths first + ); } } @@ -93,10 +98,14 @@ export class CommitSplit { // in this edge-case we should not attempt to update the path. if (splitPath.length === 1) continue; - let pkgName; + let pkgName: string | undefined; if (this.packagePaths) { // only track paths under this.packagePaths - pkgName = this.packagePaths.find(p => file.indexOf(`${p}/`) === 0); + pkgName = Object.entries(this.packagePaths).find( + ([path, additionalPaths]) => + file.indexOf(`${path}/`) === 0 || + additionalPaths.some(path => file.indexOf(`${path}/`) === 0) + )?.[0]; } else { // track paths by top level folder pkgName = splitPath[0]; @@ -108,7 +117,7 @@ export class CommitSplit { } if (commit.files.length === 0 && this.includeEmpty) { if (this.packagePaths) { - for (const pkgName of this.packagePaths) { + for (const pkgName of Object.keys(this.packagePaths)) { splitCommits[pkgName] = splitCommits[pkgName] || []; splitCommits[pkgName].push(commit); } diff --git a/src/util/commit-utils.ts b/src/util/commit-utils.ts index 9e2842763..15c230ef4 100644 --- a/src/util/commit-utils.ts +++ b/src/util/commit-utils.ts @@ -13,18 +13,20 @@ // limitations under the License. export const normalizePaths = (paths: string[]) => { - return paths.map(path => { - // normalize so that all paths have leading and trailing slashes for - // non-overlap validation. - // NOTE: GitHub API always returns paths using the `/` separator, - // regardless of what platform the client code is running on - let newPath = path.replace(/\/$/, ''); - newPath = newPath.replace(/^\//, ''); - newPath = newPath.replace(/$/, '/'); - newPath = newPath.replace(/^/, '/'); - // store them with leading and trailing slashes removed. - newPath = newPath.replace(/\/$/, ''); - newPath = newPath.replace(/^\//, ''); - return newPath; - }); + return paths.map(normalizePath); +}; + +export const normalizePath = (path: string) => { + // normalize so that all paths have leading and trailing slashes for + // non-overlap validation. + // NOTE: GitHub API always returns paths using the `/` separator, + // regardless of what platform the client code is running on + let newPath = path.replace(/\/$/, ''); + newPath = newPath.replace(/^\//, ''); + newPath = newPath.replace(/$/, '/'); + newPath = newPath.replace(/^/, '/'); + // store them with leading and trailing slashes removed. + newPath = newPath.replace(/\/$/, ''); + newPath = newPath.replace(/^\//, ''); + return newPath; }; diff --git a/test/fixtures/manifest/config/additional-paths.json b/test/fixtures/manifest/config/additional-paths.json new file mode 100644 index 000000000..7e7d380e8 --- /dev/null +++ b/test/fixtures/manifest/config/additional-paths.json @@ -0,0 +1,8 @@ +{ + "release-type": "simple", + "packages": { + "apps/my-app": { + "additional-paths": ["libs/my-lib"] + } + } +} diff --git a/test/manifest.ts b/test/manifest.ts index 7315dee0c..c53f59441 100644 --- a/test/manifest.ts +++ b/test/manifest.ts @@ -535,6 +535,34 @@ describe('Manifest', () => { 'path-ignore', ]); }); + it('should read additional paths from manifest', async () => { + const getFileContentsStub = sandbox.stub( + github, + 'getFileContentsOnBranch' + ); + getFileContentsStub + .withArgs('release-please-config.json', 'main') + .resolves( + buildGitHubFileContent( + fixturesPath, + 'manifest/config/additional-paths.json' + ) + ) + .withArgs('.release-please-manifest.json', 'main') + .resolves( + buildGitHubFileContent( + fixturesPath, + 'manifest/versions/versions.json' + ) + ); + const manifest = await Manifest.fromManifest( + github, + github.repository.defaultBranch + ); + expect( + manifest.repositoryConfig['apps/my-app'].additionalPaths + ).to.deep.equal(['libs/my-lib']); + }); it('should build simple plugins from manifest', async () => { const getFileContentsStub = sandbox.stub( github, @@ -3519,6 +3547,59 @@ describe('Manifest', () => { ); }); }); + + it('should update manifest for commits in additionalPaths', async () => { + mockReleases(sandbox, github, []); + mockTags(sandbox, github, [ + { + name: 'apps-myapp-v1.0.0', + sha: 'abc123', + }, + ]); + mockCommits(sandbox, github, [ + { + sha: 'aaaaaa', + message: 'fix: my-lib bugfix', + files: ['libs/my-lib/test.txt'], + }, + { + sha: 'abc123', + message: 'chore: release main', + files: [], + pullRequest: { + headBranchName: 'release-please/branches/main/components/myapp', + baseBranchName: 'main', + number: 123, + title: 'chore: release main', + body: '', + labels: [], + files: [], + sha: 'abc123', + }, + }, + ]); + const manifest = new Manifest( + github, + 'main', + { + 'apps/my-app': { + releaseType: 'simple', + component: 'myapp', + additionalPaths: ['libs/my-lib'], + }, + }, + { + 'apps/my-app': Version.parse('1.0.0'), + } + ); + const pullRequests = await manifest.buildPullRequests(); + expect(pullRequests).lengthOf(1); + const pullRequest = pullRequests[0]; + expect(pullRequest.version?.toString()).to.eql('1.0.1'); + expect(pullRequest.headRefName).to.eql( + 'release-please--branches--main--components--myapp' + ); + }); }); describe('createPullRequests', () => { diff --git a/test/util/commit-split.ts b/test/util/commit-split.ts index a8ea1ccf9..99a531f76 100644 --- a/test/util/commit-split.ts +++ b/test/util/commit-split.ts @@ -46,7 +46,7 @@ describe('CommitSplit', () => { }); it('uses path prefixes', () => { const commitSplit = new CommitSplit({ - packagePaths: ['pkg5', 'pkg6/pkg5'], + packagePaths: {pkg5: [], 'pkg6/pkg5': []}, }); const splitCommits = commitSplit.split(commits); expect(splitCommits['pkg1']).to.be.undefined; @@ -70,7 +70,7 @@ describe('CommitSplit', () => { }, ]; const commitSplit = new CommitSplit({ - packagePaths: ['core', 'core/subpackage'], + packagePaths: {core: [], 'core/subpackage': []}, }); const splitCommits = commitSplit.split(commits); expect(splitCommits['core']).lengthOf(1); @@ -90,7 +90,7 @@ describe('CommitSplit', () => { it('should separate commits with limited list of paths', () => { const commitSplit = new CommitSplit({ includeEmpty: true, - packagePaths: ['pkg1', 'pkg4'], + packagePaths: {pkg1: [], pkg4: []}, }); const splitCommits = commitSplit.split(commits); expect(splitCommits['pkg1']).lengthOf(3); @@ -114,7 +114,7 @@ describe('CommitSplit', () => { it('should separate commits with limited list of paths', () => { const commitSplit = new CommitSplit({ includeEmpty: false, - packagePaths: ['pkg1', 'pkg4'], + packagePaths: {pkg1: [], pkg4: []}, }); const splitCommits = commitSplit.split(commits); expect(splitCommits['pkg1']).lengthOf(2);