Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/manifest-releaser.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,20 @@ does _not_ update the dependencies, and the `cargo-workspace` plug-in must be
used to update dependencies and bump all dependents — this is the recommended
way of managing a Rust monorepo with release-please.

If your Cargo workspace `Cargo.toml` is not at the repository root (e.g. it is
in a `crates/` subdirectory), you can specify the `cargoWorkspacePath` option:

```json
{
"plugins": [
{
"type": "cargo-workspace",
"cargoWorkspacePath": "crates"
}
]
}
```

### maven-workspace

The `maven-workspace` plugin operates similarly to the `node-workspace` plugin,
Expand Down
27 changes: 25 additions & 2 deletions schemas/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -335,13 +335,36 @@
"required": ["type", "groupName", "components"]
},
{
"description": "Configuration for various `workspace` plugins.",
"description": "Configuration for the `cargo-workspace` plugin.",
"type": "object",
"properties": {
"type": {
"description": "The name of the plugin.",
"type": "string",
"enum": ["cargo-workspace"]
},
"updateAllPackages": {
"description": "Whether to force updating all packages regardless of the dependency tree. Defaults to `false`.",
"type": "boolean"
},
"merge": {
"description": "Whether to merge in-scope pull requests into a combined release pull request. Defaults to `true`.",
"type": "boolean"
},
"cargoWorkspacePath": {
"description": "Path to the directory containing the workspace Cargo.toml. Defaults to the repository root.",
"type": "string"
}
}
},
{
"description": "Configuration for the `maven-workspace` plugin.",
"type": "object",
"properties": {
"type": {
"description": "The name of the plugin.",
"type": "string",
"enum": ["cargo-workspace", "maven-workspace"]
"enum": ["maven-workspace"]
},
"updateAllPackages": {
"description": "Whether to force updating all packages regardless of the dependency tree. Defaults to `false`.",
Expand Down
5 changes: 3 additions & 2 deletions src/factories/plugin-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

import {
CargoWorkspacePluginConfig,
LinkedVersionPluginConfig,
PluginType,
RepositoryConfig,
Expand Down Expand Up @@ -75,9 +76,9 @@ const pluginFactories: Record<string, PluginBuilder> = {
options.repositoryConfig,
{
...options,
...(options.type as WorkspacePluginOptions),
...(options.type as CargoWorkspacePluginConfig),
merge:
(options.type as WorkspacePluginOptions).merge ??
(options.type as CargoWorkspacePluginConfig).merge ??
!options.separatePullRequests,
}
),
Expand Down
6 changes: 5 additions & 1 deletion src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ export interface WorkspacePluginConfig extends ConfigurablePluginType {
export interface NodeWorkspacePluginConfig extends WorkspacePluginConfig {
updatePeerDependencies?: boolean;
}
export interface CargoWorkspacePluginConfig extends WorkspacePluginConfig {
cargoWorkspacePath?: string;
}
export interface GroupPriorityPluginConfig extends ConfigurablePluginType {
groups: string[];
}
Expand All @@ -256,7 +259,8 @@ export type PluginType =
| LinkedVersionPluginConfig
| SentenceCasePluginConfig
| WorkspacePluginConfig
| NodeWorkspacePluginConfig;
| NodeWorkspacePluginConfig
| CargoWorkspacePluginConfig;

/**
* This is the schema of the manifest config json
Expand Down
45 changes: 39 additions & 6 deletions src/plugins/cargo-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import {CandidateReleasePullRequest, ROOT_PROJECT_PATH} from '../manifest';
import {
CandidateReleasePullRequest,
RepositoryConfig,
ROOT_PROJECT_PATH,
} from '../manifest';
import {
WorkspacePlugin,
DependencyGraph,
DependencyNode,
WorkspacePluginOptions,
addPath,
appendDependenciesSectionToChangelog,
} from './workspace';
Expand All @@ -38,6 +43,7 @@ import {BranchName} from '../util/branch-name';
import {PatchVersionUpdate} from '../versioning-strategy';
import {CargoLock} from '../updaters/rust/cargo-lock';
import {ConfigurationError} from '../errors';
import {GitHub} from '../github';
import {Strategy} from '../strategy';
import {Commit} from '../commit';
import {Release} from '../release';
Expand Down Expand Up @@ -74,6 +80,10 @@ interface CrateInfo {
manifest: CargoManifest;
}

interface CargoWorkspaceOptions extends WorkspacePluginOptions {
cargoWorkspacePath?: string;
}

/**
* The plugin analyzed a cargo workspace and will bump dependencies
* of managed packages if those dependencies are being updated.
Expand All @@ -84,6 +94,25 @@ interface CrateInfo {
export class CargoWorkspace extends WorkspacePlugin<CrateInfo> {
private strategiesByPath: Record<string, Strategy> = {};
private releasesByPath: Record<string, Release> = {};
private workspacePath: string;

constructor(
github: GitHub,
targetBranch: string,
repositoryConfig: RepositoryConfig,
options: CargoWorkspaceOptions = {}
) {
super(github, targetBranch, repositoryConfig, options);
// Normalize: strip leading "./" and trailing slashes
// so that "./crates/" becomes "crates" for consistent path joining
this.workspacePath = (options.cargoWorkspacePath ?? '')
.replace(/^\.\//, '')
.replace(/\/+$/, '');
}

private resolveWorkspacePath(file: string): string {
return this.workspacePath ? `${this.workspacePath}/${file}` : file;
}

protected async buildAllPackages(
candidates: CandidateReleasePullRequest[]
Expand All @@ -92,7 +121,7 @@ export class CargoWorkspace extends WorkspacePlugin<CrateInfo> {
candidatesByPackage: Record<string, CandidateReleasePullRequest>;
}> {
const cargoManifestContent = await this.github.getFileContentsOnBranch(
'Cargo.toml',
this.resolveWorkspacePath('Cargo.toml'),
this.targetBranch
);
const cargoManifest = parseCargoManifest(
Expand All @@ -111,11 +140,14 @@ export class CargoWorkspace extends WorkspacePlugin<CrateInfo> {
const members = (
await Promise.all(
cargoManifest.workspace.members.map(member =>
this.github.findFilesByGlobAndRef(member, this.targetBranch)
this.github.findFilesByGlobAndRef(
this.resolveWorkspacePath(member),
this.targetBranch
)
)
)
).flat();
members.push(ROOT_PROJECT_PATH);
members.push(this.workspacePath || ROOT_PROJECT_PATH);

for (const path of members) {
const manifestPath = addPath(path, 'Cargo.toml');
Expand Down Expand Up @@ -332,7 +364,8 @@ export class CargoWorkspace extends WorkspacePlugin<CrateInfo> {
candidates: CandidateReleasePullRequest[],
updatedVersions: VersionsMap
): CandidateReleasePullRequest[] {
let rootCandidate = candidates.find(c => c.path === ROOT_PROJECT_PATH);
const rootPath = this.workspacePath || ROOT_PROJECT_PATH;
let rootCandidate = candidates.find(c => c.path === rootPath);
if (!rootCandidate) {
this.logger.warn('Unable to find root candidate pull request');
rootCandidate = candidates.find(c => c.config.releaseType === 'rust');
Expand All @@ -344,7 +377,7 @@ export class CargoWorkspace extends WorkspacePlugin<CrateInfo> {

// Update the root Cargo.lock if it exists
rootCandidate.pullRequest.updates.push({
path: 'Cargo.lock',
path: this.resolveWorkspacePath('Cargo.lock'),
createIfMissing: false,
updater: new CargoLock(updatedVersions),
});
Expand Down
142 changes: 142 additions & 0 deletions test/plugins/cargo-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,148 @@ describe('CargoWorkspace plugin', () => {
}
);
});
it('handles non-root workspace path via cargo-workspace-path', async () => {
const candidates: CandidateReleasePullRequest[] = [
buildMockCandidatePullRequest(
'crates/packages/rustA',
'rust',
'1.1.2',
{
component: 'pkgA',
updates: [
buildMockPackageUpdate(
'crates/packages/rustA/Cargo.toml',
'packages/rustA/Cargo.toml'
),
],
}
),
];
stubFilesFromFixtures({
sandbox,
github,
fixturePath: fixturesPath,
files: [],
flatten: false,
targetBranch: 'main',
inlineFiles: [
['crates/Cargo.toml', '[workspace]\nmembers = ["packages/rustA"]'],
[
'crates/packages/rustA/Cargo.toml',
'[package]\nname = "pkgA"\nversion = "1.1.1"\n\n[dependencies]\ntracing = "1.0.0"',
],
],
});
sandbox
.stub(github, 'findFilesByGlobAndRef')
.withArgs('crates/packages/rustA', 'main')
.resolves(['crates/packages/rustA']);
plugin = new CargoWorkspace(
github,
'main',
{
'crates/packages/rustA': {
releaseType: 'rust',
},
},
{
cargoWorkspacePath: 'crates',
}
);
const newCandidates = await plugin.run(candidates);
expect(newCandidates).lengthOf(1);
const rustCandidate = newCandidates.find(
candidate => candidate.config.releaseType === 'rust'
);
expect(rustCandidate).to.not.be.undefined;
const updates = rustCandidate!.pullRequest.updates;
assertHasUpdate(updates, 'crates/packages/rustA/Cargo.toml');
assertHasUpdate(updates, 'crates/Cargo.lock');
});
it('walks dependency tree with non-root workspace path', async () => {
const candidates: CandidateReleasePullRequest[] = [
buildMockCandidatePullRequest(
'crates/packages/rustA',
'rust',
'1.1.2',
{
component: 'pkgA',
updates: [
buildMockPackageUpdate(
'crates/packages/rustA/Cargo.toml',
'packages/rustA/Cargo.toml'
),
],
}
),
];
stubFilesFromFixtures({
sandbox,
github,
fixturePath: fixturesPath,
files: [],
flatten: false,
targetBranch: 'main',
inlineFiles: [
[
'crates/Cargo.toml',
'[workspace]\nmembers = ["packages/rustA", "packages/rustB", "packages/rustC"]',
],
[
'crates/packages/rustA/Cargo.toml',
'[package]\nname = "pkgA"\nversion = "1.1.1"\n\n[dependencies]\ntracing = "1.0.0"',
],
[
'crates/packages/rustB/Cargo.toml',
'[package]\nname = "pkgB"\nversion = "2.2.2"\n\n[dependencies]\npkgA = { version = "1.1.1", path = "../pkgA" }',
],
[
'crates/packages/rustC/Cargo.toml',
'[package]\nname = "pkgC"\nversion = "3.3.3"\n\n[dependencies]\npkgB = { version = "2.2.2", path = "../pkgB" }',
],
],
});
sandbox
.stub(github, 'findFilesByGlobAndRef')
.withArgs('crates/packages/rustA', 'main')
.resolves(['crates/packages/rustA'])
.withArgs('crates/packages/rustB', 'main')
.resolves(['crates/packages/rustB'])
.withArgs('crates/packages/rustC', 'main')
.resolves(['crates/packages/rustC']);
plugin = new CargoWorkspace(
github,
'main',
{
'crates/packages/rustA': {
releaseType: 'rust',
},
'crates/packages/rustB': {
releaseType: 'rust',
},
'crates/packages/rustC': {
releaseType: 'rust',
},
},
{
cargoWorkspacePath: 'crates',
}
);
const newCandidates = await plugin.run(candidates);
expect(newCandidates).lengthOf(1);
const rustCandidate = newCandidates.find(
candidate => candidate.config.releaseType === 'rust'
);
expect(rustCandidate).to.not.be.undefined;
const updates = rustCandidate!.pullRequest.updates;
// pkgA is directly released
assertHasUpdate(updates, 'crates/packages/rustA/Cargo.toml', RawContent);
// pkgB depends on pkgA, should be bumped
assertHasUpdate(updates, 'crates/packages/rustB/Cargo.toml', RawContent);
// pkgC depends on pkgB, should be transitively bumped
assertHasUpdate(updates, 'crates/packages/rustC/Cargo.toml', RawContent);
assertHasUpdate(updates, 'crates/Cargo.lock');
});
it('handles packages with invalid version', async () => {
const candidates: CandidateReleasePullRequest[] = [
buildMockCandidatePullRequest('packages/rustA', 'rust', '1.1.2', {
Expand Down
Loading