Skip to content
Merged
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
10 changes: 6 additions & 4 deletions __snapshots__/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ Options:
[choices: "bazel", "dart", "dotnet-yoshi", "elixir", "expo", "go",
"go-librarian", "go-yoshi", "helm", "java", "java-backport", "java-bom",
"java-lts", "java-yoshi", "java-yoshi-mono-repo", "krm-blueprint", "maven",
"node", "node-librarian", "ocaml", "php", "php-yoshi", "python", "r", "ruby",
"ruby-yoshi", "rust", "salesforce", "sfdx", "simple", "terraform-module"]
"node", "node-librarian", "ocaml", "php", "php-yoshi", "python",
"python-librarian", "r", "ruby", "ruby-yoshi", "rust", "salesforce", "sfdx",
"simple", "terraform-module"]
--config-file where can the config file be found in the
project? [default: "release-please-config.json"]
--manifest-file where can the manifest file be found in the
Expand Down Expand Up @@ -279,8 +280,9 @@ Options:
[choices: "bazel", "dart", "dotnet-yoshi", "elixir", "expo", "go",
"go-librarian", "go-yoshi", "helm", "java", "java-backport", "java-bom",
"java-lts", "java-yoshi", "java-yoshi-mono-repo", "krm-blueprint", "maven",
"node", "node-librarian", "ocaml", "php", "php-yoshi", "python", "r", "ruby",
"ruby-yoshi", "rust", "salesforce", "sfdx", "simple", "terraform-module"]
"node", "node-librarian", "ocaml", "php", "php-yoshi", "python",
"python-librarian", "r", "ruby", "ruby-yoshi", "rust", "salesforce", "sfdx",
"simple", "terraform-module"]
--config-file where can the config file be found in the
project?
[default: "release-please-config.json"]
Expand Down
37 changes: 37 additions & 0 deletions __snapshots__/python-librarian.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
exports['PythonLibrarian buildUpdates updates changelog.json if present 1'] = `
{
"entries": [
{
"changes": [
{
"type": "fix",
"sha": "845db1381b3d5d20151cad2588f85feb",
"message": "update dependency com.google.cloud:google-cloud-storage to v1.120.0",
"issues": [],
"scope": "deps"
},
{
"type": "chore",
"sha": "b3f8966b023b8f21ce127142aa91841c",
"message": "update a very important dep",
"issues": [],
"breakingChangeNote": "update a very important dep"
},
{
"type": "fix",
"sha": "08ca01180a91c0a1ba8992b491db9212",
"message": "update dependency com.google.cloud:google-cloud-spanner to v1.50.0",
"issues": [],
"scope": "deps"
}
],
"version": "0.1.0",
"language": "PYTHON",
"artifactName": "google-cloud-automl",
"id": "abc-123-efd-qwerty",
"createTime": "2023-01-05T16:42:33.446Z"
}
],
"updateTime": "2023-01-05T16:42:33.446Z"
}
`
2 changes: 2 additions & 0 deletions src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {OCaml} from './strategies/ocaml';
import {PHP} from './strategies/php';
import {PHPYoshi} from './strategies/php-yoshi';
import {Python} from './strategies/python';
import {PythonLibrarian} from './strategies/python-librarian';
import {R} from './strategies/r';
import {Ruby} from './strategies/ruby';
import {RubyYoshi} from './strategies/ruby-yoshi';
Expand Down Expand Up @@ -103,6 +104,7 @@ const releasers: Record<string, ReleaseBuilder> = {
php: options => new PHP(options),
'php-yoshi': options => new PHPYoshi(options),
python: options => new Python(options),
'python-librarian': options => new PythonLibrarian(options),
r: options => new R(options),
ruby: options => new Ruby(options),
'ruby-yoshi': options => new RubyYoshi(options),
Expand Down
38 changes: 38 additions & 0 deletions src/strategies/python-librarian.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {Python} from './python';
import {BuildUpdatesOptions} from './base';
import {Update} from '../update';
import {LibrarianYamlUpdater} from '../updaters/python/librarian-yaml';

export class PythonLibrarian extends Python {
protected async buildUpdates(
options: BuildUpdatesOptions
): Promise<Update[]> {
const updates = await super.buildUpdates(options);

// Update librarian.yaml if this package exists within it.
updates.push({
path: 'librarian.yaml',
createIfMissing: false,
updater: new LibrarianYamlUpdater({
version: options.newVersion,
packagePath: this.path,
}),
});

return updates;
}
}
95 changes: 95 additions & 0 deletions src/updaters/python/librarian-yaml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {DefaultUpdater, UpdateOptions} from '../default';
import * as yaml from 'yaml';
import {logger as defaultLogger, Logger} from '../../util/logger';

export interface LibrarianLibrary {
name: string;
output: string;
version: string;
[key: string]: any;
}

export interface LibrarianYamlSchema {
libraries: LibrarianLibrary[];
[key: string]: any;
}

export interface LibrarianUpdateOptions extends UpdateOptions {
packagePath: string;
}

/**
* Updates a librarian.yaml file.
*/
export class LibrarianYamlUpdater extends DefaultUpdater {
private readonly packagePath: string;

constructor(options: LibrarianUpdateOptions) {
super(options);
this.packagePath = options.packagePath;
}

/**
* Given initial file contents, return updated contents.
* @param {string} content The initial content
* @returns {string} The updated content
*/
updateContent(content: string, logger: Logger = defaultLogger): string {
// Use yaml package to make sure librarian.yaml is not reformatted because
// we use different tool to format librarian.yaml.
const doc = yaml.parseDocument(content);
if (!doc || doc.errors.length > 0) {
logger.warn('Invalid yaml, cannot be parsed');
return content;
}

const libraries = doc.get('libraries');
if (!libraries || !yaml.isSeq(libraries)) {
return content;
}

let modified = false;
for (const library of libraries.items) {
if (!yaml.isMap(library)) continue;

// The release-please version map key is the output directory (explicit or
// derived) in librarian.yaml.
const outputDirectory = this.deriveOutputDirectory(
library.toJSON() as LibrarianLibrary
);
if (outputDirectory === this.packagePath) {
const newVersion = this.version.toString();
if (library.get('version') !== newVersion) {
library.set('version', newVersion);
modified = true;
}
}
}

if (modified) {
return doc.toString({lineWidth: 0});
}
return content;
}

deriveOutputDirectory(library: LibrarianLibrary): string {
if (library.output) {
return library.output;
}
return `packages/${library.name}`;
}
}
127 changes: 127 additions & 0 deletions test/strategies/python-librarian.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {describe, it, afterEach, beforeEach} from 'mocha';
import {expect} from 'chai';
import {GitHub} from '../../src/github';
import {PythonLibrarian} from '../../src/strategies/python-librarian';
import * as sinon from 'sinon';
import {buildGitHubFileContent, assertHasUpdate} from '../helpers';
import {buildMockConventionalCommit} from '../helpers';
import {SetupPy} from '../../src/updaters/python/setup-py';
import {LibrarianYamlUpdater} from '../../src/updaters/python/librarian-yaml';
import {Version} from '../../src/version';
import {TagName} from '../../src/util/tag-name';

const sandbox = sinon.createSandbox();
const fixturesPath = './test/fixtures/strategies/python';
const COMMITS = [
...buildMockConventionalCommit(
'fix(deps): update dependency com.google.cloud:google-cloud-storage to v1.120.0'
),
];

describe('PythonLibrarian', () => {
let github: GitHub;
beforeEach(async () => {
github = await GitHub.create({
owner: 'googleapis',
repo: 'python-librarian-test-repo',
defaultBranch: 'main',
});
});
afterEach(() => {
sandbox.restore();
});

describe('buildReleasePullRequest', () => {
it('returns release PR changes with defaultInitialVersion', async () => {
const expectedVersion = '0.1.0';
const strategy = new PythonLibrarian({
targetBranch: 'main',
github,
component: 'google-cloud-automl',
});
sandbox
.stub(github, 'getFileContentsOnBranch')
.resolves(buildGitHubFileContent(fixturesPath, 'setup.py'));
sandbox.stub(github, 'findFilesByFilenameAndRef').resolves([]);
const latestRelease = undefined;
const release = await strategy.buildReleasePullRequest(
COMMITS,
latestRelease
);
expect(release!.version?.toString()).to.eql(expectedVersion);
});

it('returns release PR changes with semver patch bump', async () => {
const expectedVersion = '0.123.5';
const strategy = new PythonLibrarian({
targetBranch: 'main',
github,
component: 'google-cloud-automl',
});
sandbox
.stub(github, 'getFileContentsOnBranch')
.resolves(buildGitHubFileContent(fixturesPath, 'setup.py'));
sandbox.stub(github, 'findFilesByFilenameAndRef').resolves([]);
const latestRelease = {
tag: new TagName(Version.parse('0.123.4'), 'google-cloud-automl'),
sha: 'abc123',
notes: 'some notes',
};
const release = await strategy.buildReleasePullRequest(
COMMITS,
latestRelease
);
expect(release!.version?.toString()).to.eql(expectedVersion);
});
});

describe('buildUpdates', () => {
it('builds common files and appends librarian.yaml correctly', async () => {
const strategy = new PythonLibrarian({
targetBranch: 'main',
github,
component: 'google-cloud-automl',
});
sandbox
.stub(github, 'getFileContentsOnBranch')
.resolves(buildGitHubFileContent(fixturesPath, 'setup.py'));
sandbox.stub(github, 'findFilesByFilenameAndRef').resolves([]);
const latestRelease = undefined;
const release = await strategy.buildReleasePullRequest(
COMMITS,
latestRelease
);
const updates = release!.updates;

// Verify that standard python updates are generated (inherited from Python strategy)
assertHasUpdate(updates, 'setup.py', SetupPy);

// Verify that librarian.yaml is correctly registered as an update
const update = assertHasUpdate(
updates,
'librarian.yaml',
LibrarianYamlUpdater
);
expect(update.createIfMissing).to.be.false;

// Verify updater is correctly configured
const updater = update.updater as LibrarianYamlUpdater;
expect(updater.version?.toString()).to.eql('0.1.0');
expect((updater as any).packagePath).to.eql('.');
});
});
});
Loading
Loading