Skip to content

Commit b246950

Browse files
committed
feat: update librarian.yaml for Node
This introduces a new Node strategy specifically for https://github.com/googleapis/google-cloud-node, with an updater which is aware of the expected Librarian format for Node. Once Librarian has moved off release-please, the strategy and updater can be removed. Towards googleapis/librarian#5461
1 parent 3ab7aec commit b246950

5 files changed

Lines changed: 630 additions & 1 deletion

File tree

src/strategies/node-librarian.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Copyright 2026 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 {BaseStrategy, BuildUpdatesOptions} from './base';
16+
import {Update} from '../update';
17+
import {ChangelogJson} from '../updaters/changelog-json';
18+
import {PackageLockJson} from '../updaters/node/package-lock-json';
19+
import {SamplesPackageJson} from '../updaters/node/samples-package-json';
20+
import {Changelog} from '../updaters/changelog';
21+
import {PackageJson} from '../updaters/node/package-json';
22+
import {LibrarianYamlUpdater} from '../updaters/node/librarian-yaml';
23+
import {GitHubFileContents} from '@google-automations/git-file-utils';
24+
import {FileNotFoundError, MissingRequiredFileError} from '../errors';
25+
import {filterCommits} from '../util/filter-commits';
26+
27+
export class NodeLibrarian extends BaseStrategy {
28+
private pkgJsonContents?: GitHubFileContents;
29+
30+
protected async buildUpdates(
31+
options: BuildUpdatesOptions
32+
): Promise<Update[]> {
33+
const updates: Update[] = [];
34+
const version = options.newVersion;
35+
const versionsMap = options.versionsMap;
36+
const packageName = (await this.getPackageName()) ?? '';
37+
const lockFiles = ['package-lock.json', 'npm-shrinkwrap.json'];
38+
const librarianFilesSearch = this.github.findFilesByFilenameAndRef(
39+
'librarian.yaml',
40+
this.targetBranch,
41+
this.path
42+
);
43+
lockFiles.forEach(lockFile => {
44+
updates.push({
45+
path: this.addPath(lockFile),
46+
createIfMissing: false,
47+
updater: new PackageLockJson({
48+
version,
49+
versionsMap,
50+
}),
51+
});
52+
});
53+
54+
updates.push({
55+
path: this.addPath('samples/package.json'),
56+
createIfMissing: false,
57+
updater: new SamplesPackageJson({
58+
version,
59+
packageName,
60+
}),
61+
});
62+
63+
!this.skipChangelog &&
64+
updates.push({
65+
path: this.addPath(this.changelogPath),
66+
createIfMissing: true,
67+
updater: new Changelog({
68+
version,
69+
changelogEntry: options.changelogEntry,
70+
}),
71+
});
72+
73+
updates.push({
74+
path: this.addPath('package.json'),
75+
createIfMissing: false,
76+
cachedFileContents: this.pkgJsonContents,
77+
updater: new PackageJson({
78+
version,
79+
}),
80+
});
81+
82+
// If a machine readable changelog.json exists update it:
83+
if (options.commits && packageName && !this.skipChangelog) {
84+
const commits = filterCommits(options.commits, this.changelogSections);
85+
updates.push({
86+
path: 'changelog.json',
87+
createIfMissing: false,
88+
updater: new ChangelogJson({
89+
artifactName: packageName,
90+
version,
91+
commits,
92+
language: 'JAVASCRIPT',
93+
}),
94+
});
95+
}
96+
97+
// Update librarian.yaml for every matching library.
98+
const librarianFiles = await librarianFilesSearch;
99+
librarianFiles.forEach(path => {
100+
updates.push({
101+
path: this.addPath(path),
102+
createIfMissing: false,
103+
updater: new LibrarianYamlUpdater({
104+
version,
105+
versionsMap,
106+
}),
107+
});
108+
});
109+
110+
return updates;
111+
}
112+
113+
async getDefaultPackageName(): Promise<string | undefined> {
114+
const pkgJsonContents = await this.getPkgJsonContents();
115+
const pkg = JSON.parse(pkgJsonContents.parsedContent);
116+
return pkg.name;
117+
}
118+
119+
protected normalizeComponent(component: string | undefined): string {
120+
if (!component) {
121+
return '';
122+
}
123+
return component.match(/^@[\w-]+\//) ? component.split('/')[1] : component;
124+
}
125+
126+
protected async getPkgJsonContents(): Promise<GitHubFileContents> {
127+
if (!this.pkgJsonContents) {
128+
try {
129+
this.pkgJsonContents = await this.github.getFileContentsOnBranch(
130+
this.addPath('package.json'),
131+
this.targetBranch
132+
);
133+
} catch (e) {
134+
if (e instanceof FileNotFoundError) {
135+
throw new MissingRequiredFileError(
136+
this.addPath('package.json'),
137+
'node',
138+
`${this.repository.owner}/${this.repository.repo}`
139+
);
140+
}
141+
throw e;
142+
}
143+
}
144+
return this.pkgJsonContents;
145+
}
146+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright 2026 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 {DefaultUpdater} from '../default';
16+
import * as yaml from 'yaml';
17+
import {logger as defaultLogger, Logger} from '../../util/logger';
18+
19+
export interface LibrarianLibrary {
20+
name: string;
21+
output: string;
22+
version: string;
23+
[key: string]: any;
24+
}
25+
26+
export interface LibrarianYamlSchema {
27+
libraries: LibrarianLibrary[];
28+
[key: string]: any;
29+
}
30+
31+
/**
32+
* Updates a librarian.yaml file.
33+
*/
34+
export class LibrarianYamlUpdater extends DefaultUpdater {
35+
/**
36+
* Given initial file contents, return updated contents.
37+
* @param {string} content The initial content
38+
* @returns {string} The updated content
39+
*/
40+
updateContent(content: string, logger: Logger = defaultLogger): string {
41+
if (!this.versionsMap) {
42+
logger.warn('missing versions map');
43+
return content;
44+
}
45+
46+
// Use yaml package to make sure librarian.yaml is not reformatted because
47+
// we use different tool to format librarian.yaml.
48+
const doc = yaml.parseDocument(content);
49+
if (!doc || doc.errors.length > 0) {
50+
logger.warn('Invalid yaml, cannot be parsed');
51+
return content;
52+
}
53+
54+
const libraries = doc.get('libraries');
55+
if (!libraries || !yaml.isSeq(libraries)) {
56+
return content;
57+
}
58+
59+
let modified = false;
60+
for (const library of libraries.items) {
61+
if (!yaml.isMap(library)) continue;
62+
63+
// The release-please version map key is the output directory (explicit or
64+
// derived) in librarian.yaml.
65+
const outputDirectory = this.deriveOutputDirectory(
66+
library.toJSON() as LibrarianLibrary
67+
);
68+
if (this.versionsMap.has(outputDirectory)) {
69+
const newVersion = this.versionsMap.get(outputDirectory);
70+
if (newVersion && library.get('version') !== newVersion.toString()) {
71+
library.set('version', newVersion.toString());
72+
modified = true;
73+
}
74+
}
75+
}
76+
77+
if (modified) {
78+
return doc.toString({lineWidth: 0});
79+
}
80+
return content;
81+
}
82+
83+
deriveOutputDirectory(library: LibrarianLibrary): string {
84+
if (library.output) {
85+
return library.output;
86+
}
87+
return `packages/${library.name}`;
88+
}
89+
}

0 commit comments

Comments
 (0)