Skip to content

Commit

Permalink
feat: add strategy for R packages (#2436)
Browse files Browse the repository at this point in the history
* feat: add strategy for R packages. Closes #2151

* chore: format and fix linting errors

* chore: remove unused package name getter

* chore: upload snapshot

* chore: remove unused module

* chore: add license header

* test: fix snapshot, license years

---------

Co-authored-by: Jeff Ching <[email protected]>
  • Loading branch information
jolars and chingor13 authored Jan 6, 2025
1 parent 91f5078 commit 2b5ff3b
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ Release Please automates releases for the following flavors of repositories:
| `ocaml` | [An OCaml repository, containing 1 or more opam or esy files and a CHANGELOG.md](https://github.com/grain-lang/binaryen.ml) |
| `php` | A repository with a composer.json and a CHANGELOG.md |
| `python` | [A Python repository, with a setup.py, setup.cfg, CHANGELOG.md](https://github.com/googleapis/python-storage) and optionally a pyproject.toml and a &lt;project&gt;/\_\_init\_\_.py |
| `R` | A repository with a DESCRIPTION and a NEWS.md |
| `ruby` | A repository with a version.rb and a CHANGELOG.md |
| `rust` | A Rust repository, with a Cargo.toml (either as a crate or workspace, although note that workspaces require a [manifest driven release](https://github.com/googleapis/release-please/blob/main/docs/manifest-releaser.md) and the "cargo-workspace" plugin) and a CHANGELOG.md |
| `sfdx` | A repository with a [sfdx-project.json](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_ws_config.htm) and a CHANGELOG.md |
Expand Down
8 changes: 4 additions & 4 deletions __snapshots__/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ Options:
[choices: "bazel", "dart", "dotnet-yoshi", "elixir", "expo", "go", "go-yoshi",
"helm", "java", "java-backport", "java-bom", "java-lts", "java-yoshi",
"java-yoshi-mono-repo", "krm-blueprint", "maven", "node", "ocaml", "php",
"php-yoshi", "python", "ruby", "ruby-yoshi", "rust", "salesforce", "sfdx",
"simple", "terraform-module"]
"php-yoshi", "python", "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 @@ -251,8 +251,8 @@ Options:
[choices: "bazel", "dart", "dotnet-yoshi", "elixir", "expo", "go", "go-yoshi",
"helm", "java", "java-backport", "java-bom", "java-lts", "java-yoshi",
"java-yoshi-mono-repo", "krm-blueprint", "maven", "node", "ocaml", "php",
"php-yoshi", "python", "ruby", "ruby-yoshi", "rust", "salesforce", "sfdx",
"simple", "terraform-module"]
"php-yoshi", "python", "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
14 changes: 14 additions & 0 deletions __snapshots__/r-description.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
exports['DESCRIPTION updateContent updates version in DESCRIPTION file 1'] = `
Package: mypackage
Title: What the Package Does (One Line, Title Case)
Version: 1.2.3
Authors@R:
person("First", "Last", , "[email protected]", role = c("aut", "cre"),
comment = c(ORCID = "YOUR-ORCID-ID"))
Description: What the package does (one paragraph).
License: GPL-3
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.2
`
2 changes: 2 additions & 0 deletions src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {OCaml} from './strategies/ocaml';
import {PHP} from './strategies/php';
import {PHPYoshi} from './strategies/php-yoshi';
import {Python} from './strategies/python';
import {R} from './strategies/r';
import {Ruby} from './strategies/ruby';
import {RubyYoshi} from './strategies/ruby-yoshi';
import {Rust} from './strategies/rust';
Expand Down Expand Up @@ -98,6 +99,7 @@ const releasers: Record<string, ReleaseBuilder> = {
php: options => new PHP(options),
'php-yoshi': options => new PHPYoshi(options),
python: options => new Python(options),
r: options => new R(options),
ruby: options => new Ruby(options),
'ruby-yoshi': options => new RubyYoshi(options),
rust: options => new Rust(options),
Expand Down
72 changes: 72 additions & 0 deletions src/strategies/r.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2025 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 {BaseStrategy, BuildUpdatesOptions, BaseStrategyOptions} from './base';
import {Update} from '../update';
import {News} from '../updaters/r/news';
import {Version} from '../version';
import {DescriptionUpdater} from '../updaters/r/description';

const CHANGELOG_SECTIONS = [
{type: 'feat', section: 'Features'},
{type: 'fix', section: 'Bug Fixes'},
{type: 'perf', section: 'Performance Improvements'},
{type: 'deps', section: 'Dependencies'},
{type: 'revert', section: 'Reverts'},
{type: 'docs', section: 'Documentation'},
{type: 'style', section: 'Styles', hidden: true},
{type: 'chore', section: 'Miscellaneous Chores', hidden: true},
{type: 'refactor', section: 'Code Refactoring', hidden: true},
{type: 'test', section: 'Tests', hidden: true},
{type: 'build', section: 'Build System', hidden: true},
{type: 'ci', section: 'Continuous Integration', hidden: true},
];

export class R extends BaseStrategy {
constructor(options: BaseStrategyOptions) {
options.changelogPath = options.changelogPath ?? 'NEWS.md';
options.changelogSections = options.changelogSections ?? CHANGELOG_SECTIONS;
super(options);
}

protected async buildUpdates(
options: BuildUpdatesOptions
): Promise<Update[]> {
const updates: Update[] = [];
const version = options.newVersion;

updates.push({
path: this.addPath(this.changelogPath),
createIfMissing: true,
updater: new News({
version,
changelogEntry: options.changelogEntry,
}),
});

updates.push({
path: this.addPath('DESCRIPTION'),
createIfMissing: false,
updater: new DescriptionUpdater({
version,
}),
});

return updates;
}

protected initialReleaseVersion(): Version {
return Version.parse('0.1.0');
}
}
32 changes: 32 additions & 0 deletions src/updaters/r/description.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2025 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} from '../default';

/**
* Updates the DESCRIPTION file of an R package.
*/
export class DescriptionUpdater extends DefaultUpdater {
/**
* Given initial file contents, return updated contents.
* @param {string} content The initial content
* @returns {string} The updated content
*/
updateContent(content: string): string {
return content.replace(
/^Version:\s*[0-9]+\.[0-9]+\.[0-9]+(?:\.[0-9]+)?\s*$/m,
`Version: ${this.version}`
);
}
}
57 changes: 57 additions & 0 deletions src/updaters/r/news.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2025 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';

interface ChangelogOptions extends UpdateOptions {
changelogEntry: string;
versionHeaderRegex?: string;
}

const DEFAULT_VERSION_HEADER_REGEX = '\n### v?[0-9[]';

export class News extends DefaultUpdater {
changelogEntry: string;
readonly versionHeaderRegex: RegExp;

constructor(options: ChangelogOptions) {
super(options);
this.changelogEntry = options.changelogEntry;
this.versionHeaderRegex = new RegExp(
options.versionHeaderRegex ?? DEFAULT_VERSION_HEADER_REGEX,
's'
);
}

updateContent(content: string | undefined): string {
content = content || '';
const lastEntryIndex = content.search(this.versionHeaderRegex);
if (lastEntryIndex === -1) {
if (content) {
return `${this.changelogEntry}\n\n${adjustHeaders(content).trim()}\n`;
} else {
return `${this.changelogEntry}\n`;
}
} else {
const before = content.slice(0, lastEntryIndex);
const after = content.slice(lastEntryIndex);
return `${before}\n${this.changelogEntry}\n${after}`.trim() + '\n';
}
}
}

// Helper to increase markdown H1 headers to H2
function adjustHeaders(content: string): string {
return content.replace(/^#(\s)/gm, '##$1');
}
72 changes: 72 additions & 0 deletions test/strategies/r.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2025 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 {afterEach, beforeEach, describe, it} from 'mocha';
import {GitHub} from '../../src';
import * as sinon from 'sinon';
import {
assertHasUpdate,
assertHasUpdates,
buildMockConventionalCommit,
} from '../helpers';
import {News} from '../../src/updaters/r/news';
import {DescriptionUpdater} from '../../src/updaters/r/description';
import {R} from '../../src/strategies/r';
import {expect} from 'chai';

const sandbox = sinon.createSandbox();

const COMMITS = [
...buildMockConventionalCommit('fix(deps): update dependency'),
...buildMockConventionalCommit('chore: update common templates'),
];

describe('R', () => {
let github: GitHub;
beforeEach(async () => {
github = await GitHub.create({
owner: 'googleapis',
repo: 'r-test-repo',
defaultBranch: 'main',
});
});
afterEach(() => {
sandbox.restore();
});
describe('buildReleasePullRequest', () => {
it('updates DESCRIPTION and NEWS.md files', async () => {
const strategy = new R({
targetBranch: 'main',
github,
changelogPath: 'NEWS.md',
});

sandbox
.stub(github, 'findFilesByFilenameAndRef')
.withArgs('DESCRIPTION', 'main')
.resolves(['DESCRIPTION']);

const release = await strategy.buildReleasePullRequest(
COMMITS,
undefined
);

expect(release?.version?.toString()).to.eql('0.1.0');

const updates = release!.updates;
assertHasUpdate(updates, 'NEWS.md', News);
assertHasUpdates(updates, 'DESCRIPTION', DescriptionUpdater);
});
});
});
11 changes: 11 additions & 0 deletions test/updaters/fixtures/r/DESCRIPTION
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Package: mypackage
Title: What the Package Does (One Line, Title Case)
Version: 0.0.1
Authors@R:
person("First", "Last", , "[email protected]", role = c("aut", "cre"),
comment = c(ORCID = "YOUR-ORCID-ID"))
Description: What the package does (one paragraph).
License: GPL-3
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.2
38 changes: 38 additions & 0 deletions test/updaters/r-description.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2025 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 {readFileSync} from 'fs';
import {resolve} from 'path';
import * as snapshot from 'snap-shot-it';
import {describe, it} from 'mocha';
import {DescriptionUpdater} from '../../src/updaters/r/description';
import {Version} from '../../src/version';

const fixturesPath = './test/updaters/fixtures';

describe('DESCRIPTION', () => {
describe('updateContent', () => {
it('updates version in DESCRIPTION file', async () => {
const oldContent = readFileSync(
resolve(fixturesPath, './r/DESCRIPTION'),
'utf8'
).replace(/\r\n/g, '\n');
const version = new DescriptionUpdater({
version: Version.parse('1.2.3'),
});
const newContent = version.updateContent(oldContent);
snapshot(newContent);
});
});
});

0 comments on commit 2b5ff3b

Please sign in to comment.