Skip to content

Commit

Permalink
feat: add strategy for R packages. Closes googleapis#2151
Browse files Browse the repository at this point in the history
  • Loading branch information
jolars committed Nov 19, 2024
1 parent 3e80797 commit 414d1bb
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 0 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 <project>/\_\_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
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
94 changes: 94 additions & 0 deletions src/strategies/r.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2024 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';
import { FileNotFoundError } from '../errors';
import { filterCommits } from '../util/filter-commits';

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 async getPackageNameFromDescription(): Promise<string | null> {
try {
const descriptionContent = (
await this.github.getFileContentsOnBranch(
this.addPath('DESCRIPTION'),
this.targetBranch
)
).parsedContent;

const match = descriptionContent.match(/^Package:\s*(.+)\s*$/m);
return match ? match[1] : null;
} catch (e) {
if (e instanceof FileNotFoundError) {
return null;
} else {
throw e;
}
}
}

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 2024 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}`
);
}
}
59 changes: 59 additions & 0 deletions src/updaters/r/news.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2024 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');
}
61 changes: 61 additions & 0 deletions test/strategies/r.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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 {Generic} from '../../src/updaters/generic';
import {DescriptionUpdater} from '../../src/updaters/r/description';
import {R} from '../../src/strategies/r';
import {TagName} from '../../src/util/tag-name';
import {Version} from '../../src/version';
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", , "first.last@example.com", 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 2024 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 414d1bb

Please sign in to comment.