Skip to content

Commit a9bed82

Browse files
feat: allow excluding file paths to filter out commits (#1875)
* feat: add exclude for manifest packages * chore: adjust ReleaserConfigJson interface * docs: update documentation * feat: read exclude-paths from configuration * chore: add license to new files * fix: include only based on relevant files * use array index, Array.prototype.at is not available in node 14 which we still support --------- Co-authored-by: Jeff Ching <chingor@google.com>
1 parent fde2602 commit a9bed82

9 files changed

Lines changed: 305 additions & 25 deletions

File tree

docs/manifest-releaser.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,8 @@ defaults (those are documented in comments)
262262
".": {
263263
// overrides release-type for node
264264
"release-type": "node",
265+
// exclude commits from that path from processing
266+
"exclude-paths": ["path/to/myPyPkgA"]
265267
},
266268

267269
// path segment should be relative to repository root

schemas/config.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,13 @@
184184
]
185185
}
186186
},
187+
"exclude-paths": {
188+
"description": "Path of commits to be excluded from parsing. If all files from commit belong to one of the paths it will be skipped",
189+
"type": "array",
190+
"items": {
191+
"type": "string"
192+
}
193+
},
187194
"version-file": {
188195
"description": "Path to the specialize version file. Used by `ruby` and `simple` strategies.",
189196
"type": "string"
@@ -394,6 +401,7 @@
394401
"extra-files": true,
395402
"version-file": true,
396403
"snapshot-label": true,
397-
"initial-version": true
404+
"initial-version": true,
405+
"exclude-paths": true
398406
}
399407
}

src/manifest.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
FilePullRequestOverflowHandler,
4747
} from './util/pull-request-overflow-handler';
4848
import {signoffCommitMessage} from './util/signoff-commit-message';
49+
import {CommitExclude} from './util/commit-exclude';
4950

5051
type ExtraJsonFile = {
5152
type: 'json';
@@ -125,6 +126,8 @@ export interface ReleaserConfig {
125126
extraFiles?: ExtraFile[];
126127
snapshotLabels?: string[];
127128
skipSnapshot?: boolean;
129+
// Manifest only
130+
excludePaths?: string[];
128131
}
129132

130133
export interface CandidateReleasePullRequest {
@@ -167,6 +170,7 @@ interface ReleaserConfigJson {
167170
'snapshot-label'?: string; // Java-only
168171
'skip-snapshot'?: boolean; // Java-only
169172
'initial-version'?: string;
173+
'exclude-paths'?: string[]; // manifest-only
170174
}
171175

172176
export interface ManifestOptions {
@@ -637,13 +641,15 @@ export class Manifest {
637641
const splitCommits = cs.split(commits);
638642

639643
// limit paths to ones since the last release
640-
const commitsPerPath: Record<string, Commit[]> = {};
644+
let commitsPerPath: Record<string, Commit[]> = {};
641645
for (const path in this.repositoryConfig) {
642646
commitsPerPath[path] = commitsAfterSha(
643647
path === ROOT_PROJECT_PATH ? commits : splitCommits[path],
644648
releaseShasByPath[path]
645649
);
646650
}
651+
const commitExclude = new CommitExclude(this.repositoryConfig);
652+
commitsPerPath = commitExclude.excludeCommits(commitsPerPath);
647653

648654
// backfill latest release tags from manifest
649655
for (const path in this.repositoryConfig) {
@@ -1282,6 +1288,7 @@ function extractReleaserConfig(
12821288
extraLabels: config['extra-label']?.split(','),
12831289
skipSnapshot: config['skip-snapshot'],
12841290
initialVersion: config['initial-version'],
1291+
excludePaths: config['exclude-paths'],
12851292
};
12861293
}
12871294

@@ -1616,6 +1623,7 @@ function mergeReleaserConfig(
16161623
skipSnapshot: pathConfig.skipSnapshot ?? defaultConfig.skipSnapshot,
16171624
initialVersion: pathConfig.initialVersion ?? defaultConfig.initialVersion,
16181625
extraLabels: pathConfig.extraLabels ?? defaultConfig.extraLabels,
1626+
excludePaths: pathConfig.excludePaths ?? defaultConfig.excludePaths,
16191627
};
16201628
}
16211629

src/util/commit-exclude.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import {Commit} from '../commit';
16+
import {ReleaserConfig, ROOT_PROJECT_PATH} from '../manifest';
17+
import {normalizePaths} from './commit-utils';
18+
19+
export type CommitExcludeConfig = Pick<ReleaserConfig, 'excludePaths'>;
20+
21+
export class CommitExclude {
22+
private excludePaths: Record<string, string[]> = {};
23+
24+
constructor(config: Record<string, CommitExcludeConfig>) {
25+
Object.entries(config).forEach(([path, releaseConfig]) => {
26+
if (releaseConfig.excludePaths) {
27+
this.excludePaths[path] = normalizePaths(releaseConfig.excludePaths);
28+
}
29+
});
30+
}
31+
32+
excludeCommits<T extends Commit>(
33+
commitsPerPath: Record<string, T[]>
34+
): Record<string, T[]> {
35+
const filteredCommitsPerPath: Record<string, T[]> = {};
36+
Object.entries(commitsPerPath).forEach(([path, commits]) => {
37+
if (this.excludePaths[path]) {
38+
commits = commits.filter(commit =>
39+
this.shouldInclude(commit, this.excludePaths[path], path)
40+
);
41+
}
42+
filteredCommitsPerPath[path] = commits;
43+
});
44+
return filteredCommitsPerPath;
45+
}
46+
47+
private shouldInclude(
48+
commit: Commit,
49+
excludePaths: string[],
50+
packagePath: string
51+
): boolean {
52+
return (
53+
!commit.files ||
54+
!commit.files
55+
.filter(file => this.isRelevant(file, packagePath))
56+
.every(file => excludePaths.some(path => this.isRelevant(file, path)))
57+
);
58+
}
59+
60+
private isRelevant(file: string, path: string) {
61+
return path === ROOT_PROJECT_PATH || file.indexOf(`${path}/`) === 0;
62+
}
63+
}

src/util/commit-split.ts

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import {Commit} from '../commit';
1616
import {ROOT_PROJECT_PATH} from '../manifest';
17+
import {normalizePaths} from './commit-utils';
1718

1819
export interface CommitSplitOptions {
1920
// Include empty git commits: each empty commit is included
@@ -54,29 +55,14 @@ export class CommitSplit {
5455
opts = opts || {};
5556
this.includeEmpty = !!opts.includeEmpty;
5657
if (opts.packagePaths) {
57-
const paths: string[] = [];
58-
for (let newPath of opts.packagePaths) {
59-
// The special "." path, representing the root of the module, should be
60-
// ignored by commit-split as it is assigned all commits in manifest.ts
61-
if (newPath === ROOT_PROJECT_PATH) {
62-
continue;
63-
}
64-
// normalize so that all paths have leading and trailing slashes for
65-
// non-overlap validation.
66-
// NOTE: GitHub API always returns paths using the `/` separator,
67-
// regardless of what platform the client code is running on
68-
newPath = newPath.replace(/\/$/, '');
69-
newPath = newPath.replace(/^\//, '');
70-
newPath = newPath.replace(/$/, '/');
71-
newPath = newPath.replace(/^/, '/');
72-
// store them with leading and trailing slashes removed.
73-
newPath = newPath.replace(/\/$/, '');
74-
newPath = newPath.replace(/^\//, '');
75-
paths.push(newPath);
76-
}
77-
78-
// sort by longest paths first
79-
this.packagePaths = paths.sort((a, b) => b.length - a.length);
58+
const paths: string[] = normalizePaths(opts.packagePaths);
59+
this.packagePaths = paths
60+
.filter(path => {
61+
// The special "." path, representing the root of the module, should be
62+
// ignored by commit-split as it is assigned all commits in manifest.ts
63+
return path !== ROOT_PROJECT_PATH;
64+
})
65+
.sort((a, b) => b.length - a.length); // sort by longest paths first
8066
}
8167
}
8268

src/util/commit-utils.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
export const normalizePaths = (paths: string[]) => {
16+
return paths.map(path => {
17+
// normalize so that all paths have leading and trailing slashes for
18+
// non-overlap validation.
19+
// NOTE: GitHub API always returns paths using the `/` separator,
20+
// regardless of what platform the client code is running on
21+
let newPath = path.replace(/\/$/, '');
22+
newPath = newPath.replace(/^\//, '');
23+
newPath = newPath.replace(/$/, '/');
24+
newPath = newPath.replace(/^/, '/');
25+
// store them with leading and trailing slashes removed.
26+
newPath = newPath.replace(/\/$/, '');
27+
newPath = newPath.replace(/^\//, '');
28+
return newPath;
29+
});
30+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"release-type": "simple",
3+
"label": "custom: pending",
4+
"release-label": "custom: tagged",
5+
"exclude-paths": ["path-ignore"],
6+
"packages": {
7+
".": {
8+
"component": "root",
9+
"exclude-paths": ["path-root-ignore"]
10+
},
11+
"node-lib": {
12+
"component": "node-lib"
13+
}
14+
}
15+
}

test/manifest.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,37 @@ describe('Manifest', () => {
504504
'lang: nodejs',
505505
]);
506506
});
507+
it('should read exclude paths from manifest', async () => {
508+
const getFileContentsStub = sandbox.stub(
509+
github,
510+
'getFileContentsOnBranch'
511+
);
512+
getFileContentsStub
513+
.withArgs('release-please-config.json', 'main')
514+
.resolves(
515+
buildGitHubFileContent(
516+
fixturesPath,
517+
'manifest/config/exclude-paths.json'
518+
)
519+
)
520+
.withArgs('.release-please-manifest.json', 'main')
521+
.resolves(
522+
buildGitHubFileContent(
523+
fixturesPath,
524+
'manifest/versions/versions.json'
525+
)
526+
);
527+
const manifest = await Manifest.fromManifest(
528+
github,
529+
github.repository.defaultBranch
530+
);
531+
expect(manifest.repositoryConfig['.'].excludePaths).to.deep.equal([
532+
'path-root-ignore',
533+
]);
534+
expect(manifest.repositoryConfig['node-lib'].excludePaths).to.deep.equal([
535+
'path-ignore',
536+
]);
537+
});
507538
it('should build simple plugins from manifest', async () => {
508539
const getFileContentsStub = sandbox.stub(
509540
github,

0 commit comments

Comments
 (0)